Skip to content

Conversation

@yume-chan
Copy link
Owner

@yume-chan yume-chan commented Jan 6, 2026

Summary by CodeRabbit

  • New Features

    • Streaming-first video decode pipeline with pause/resume, writable renderer streams, snapshot support, and auto-select canvas renderer with optional capture.
    • New observable decode/render metrics and timestamp/decoder streams for latency insights.
  • Refactor

    • Codec and decoder APIs migrated to stream/transform primitives; renderers and flow-control unified around writable/readable streams.
    • Subprocess PTY/service types renamed for consistent public API.
  • Other

    • Added buffer concatenation helpers and public API renames for clearer naming.

✏️ Tip: You can customize this high-level summary in your review settings.

@diffray diffray bot added the diffray-review-started diffray review status: started label Jan 6, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Renamed PTY/Adb types; migrated codec decoders to TransformStream-based codecs; promoted PauseController/PerformanceCounter to public stream primitives; added streaming pipeline components (VideoDecoderStream, TimestampTransforms, RendererController, RedrawController) and many renderer/utility API changes across scrcpy and adb modules.

Changes

Cohort / File(s) Summary
ADB PTY renames
\libraries/adb/src/commands/subprocess/none/pty.ts`, `libraries/adb/src/commands/subprocess/none/service.ts`, `libraries/adb/src/commands/subprocess/pty.ts`, `libraries/adb/src/commands/subprocess/shell/pty.ts`, `libraries/adb/src/commands/subprocess/shell/service.ts``
Rename AdbPtyProcess<T>AdbPty<T>; classes renamed (AdbNoneProtocolPtyProcessAdbNoneProtocolPty, AdbShellProtocolPtyProcessAdbShellProtocolPty); update imports, implementations and factory return types.
Codec API → TransformStream migration
\libraries/scrcpy-decoder-webcodecs/src/video/codec/*``
Replace CodecDecoder with CodecTransformStream (TransformStream alias); per-codec decoders converted to Av1TransformStream, H264TransformStream, H265TransformStream, H26xTransformStream; configure() now returns Config objects; remove per-packet decode() APIs.
Decoder pipeline & utilities
\libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts`, `.../video/utils/timestamp.ts`, `.../video/utils/video-decoder-stream.ts`, `.../video/utils/index.ts`, `.../video/utils/frame-type.ts``
Added streaming components: PauseController, TimestampTransforms (and increasingNow), VideoDecoderStream (observability: decodeQueueSize/onDequeue/framesDecoded/framesSkipped/decoderResetCount), and convertFrameType; main decoder rewired to use transforms and VideoDecoderStream.
tinyh264: pause & performance
\libraries/scrcpy-decoder-tinyh264/src/types.ts`, `.../utils/pause.ts`, `.../utils/performance.ts`, `.../decoder.ts`, `.../utils/index.js```
Made PauseController a public TransformStream with typed Input/Output (data packets include skipRendering); exported PerformanceCounter; renamed counters (framesDrawnframesRendered, framesSkippedframesSkippedRendering), added framesPresented; adjusted resume()/dispose() types and decoder getters.
Renderer API & implementations
\libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts`, `...`/bitmap.ts`, `...`/webgl.ts`, `...`/insertable-stream.ts`, `...`/flow-control.ts`, `...`/auto-canvas.ts`, `...`/index.ts`, `...`/type.ts``
Replace method-based VideoFrameRenderer with writable: WritableStream<VideoFrame> API; CanvasVideoFrameRenderer made generic and exposes writable/options; added RedrawController, RendererController (TransformStream + counters); adapted Bitmap/WebGL/InsertableStream/AutoCanvas renderers.
Render flow, redraw & snapshot
\libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts`, `.../snapshot.ts`, `.../bitmap.ts`, `.../flow-control.ts``
Added RedrawController and RendererController with snapshot/capture and performance counters; added canvasToBlob helper; bitmap renderer now uses createImageBitmap transfer path and constructor-driven frame processor.
Codec utils / exports cleanup
\libraries/scrcpy-decoder-webcodecs/src/video/codec/index.ts`, `.../codec/utils.ts`, `.../utils/index.ts``
Removed re-export ./utils.js; removed small hex/decimal helpers; added/re-exported frame-type, timestamp, and video-decoder-stream utilities.
Stream & buffer utilities, misc
\libraries/stream-extra/src/concat.ts`, `libraries/struct/src/utils.ts`, `.vscode/settings.json`**`
Added toUint8Array and concatBuffers; exported SharedEncoder and declared TextEncoder.encodeInto; updated cSpell words.
PushReadable changes
\libraries/stream-extra/src/push-readable.ts`, `libraries/stream-extra/src/push-readable.spec.ts``
PushReadableStreamController.enqueue now returns Promise<boolean> to support backpressure/readiness; internal controller management and tests updated accordingly.
ADB credential storage key type adjustments
\libraries/adb-credential-web/src/storage/password.ts`, `libraries/adb-credential-web/src/storage/prf/storage.ts`**`
Changed privateKey parameter types from Uint8Array<ArrayBuffer>Uint8Array; callers now convert via toLocalUint8Array(privateKey) before encryption.
Daemon / process small runtime changes
\libraries/adb/src/daemon/dispatcher.ts`, `libraries/android-bin/src/bug-report.ts`, `libraries/scrcpy/src/2_0/impl/parse-audio-stream-metadata.ts`**`
Open/Write handling switched to fire-and-forget with controller.error propagation; spawn now receives abort signal; replaced manual reader loop with for-await iteration.
Misc exports
\libraries/media-codec/src/index.ts`, `libraries/scrcpy-decoder-tinyh264/src/decoder.ts`**, \libraries/scrcpy-decoder-tinyh264/src/types.ts``
New re-export format.js; tinyh264 exported getters changed (paused, writable, framesRendered, framesPresented, framesSkippedRendering) and resume() return type changed to undefined; exported noop.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PauseController
    participant TimestampTransforms
    participant VideoDecoderStream
    participant RendererController
    participant Renderer

    Client->>PauseController: write packets (config/data)
    PauseController-->>TimestampTransforms: output packets (adds skipRendering)
    TimestampTransforms->>VideoDecoderStream: forward timestamped packets/config
    VideoDecoderStream->>RendererController: emit decoded VideoFrame
    RendererController->>Renderer: deliver frames via writable/readable and update counters
Loading
sequenceDiagram
    participant App as WebCodecsVideoDecoder
    participant Pause as PauseController
    participant Codec as CodecTransformStream
    participant VD as VideoDecoderStream
    participant RC as RendererController

    App->>Pause: pipe input stream
    Pause->>Codec: pipe transformed packets
    Codec->>VD: emit EncodedVideoChunk or Config
    VD->>RC: output VideoFrame
    RC->>App: expose metrics, snapshot, readable/writable
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: refactoring the WebCodecs decoder/renderer architecture to implement frame skipping when components cannot keep up with the stream.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

40-45: Consider adding cleanup handlers to the WritableStream.

The WritableStream lacks abort and close handlers. When dispose() is called, the controller is disposed but the stream itself remains technically "open". While subsequent writes will throw (due to controller's error state), this may cause issues for consumers expecting proper stream termination signals.

Based on learnings, cleanup should be explicit rather than relying on implicit error propagation.

♻️ Suggested improvement
 `#writable` = new WritableStream<VideoFrame>({
     write: (frame) => this.#controller.draw(frame),
+    abort: () => {
+        this.#controller.dispose();
+    },
 });

Additionally, consider aborting the stream in dispose():

 dispose(): undefined {
+    this.#writable.abort().catch(() => {});
+
     if (this.#canvasSize !== "external") {
         this.#canvas.width = 0;
         this.#canvas.height = 0;
     }

     this.#resizeObserver?.disconnect();
     this.#controller.dispose();
 }

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2d0195 and 985983a.

📒 Files selected for processing (6)
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-12T06:58:27.724Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:27.724Z
Learning: In TransformStream implementations, calling `controller.error()` immediately clears the internal `[[flushAlgorithm]]` and `[[cancelAlgorithm]]` slots, which prevents the `flush()` and `cancel()` callbacks from being invoked. This means that if a decoder's error callback calls `controller.error()`, the stream's `cancel()` callback will not subsequently run, eliminating concerns about double-close scenarios.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
🧬 Code graph analysis (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (20-22)
  • Output (39-39)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (8)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • Options (251-261)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • Options (344-351)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (2)
  • Options (264-271)
  • canvas (40-42)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)
  • frame (115-140)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (2)
  • RedrawController (6-102)
  • draw (32-56)
libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts (1)
  • canvas (9-11)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (1)
  • createCanvas (1-9)
libraries/scrcpy-decoder-webcodecs/src/video/utils/snapshot.ts (1)
  • canvasToBlob (1-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (20)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (6)

1-7: LGTM!

Imports are well-organized and align with the new stream-based architecture.


9-38: LGTM!

The generic class design with delegated frame lifecycle to RedrawController is clean. Initializing display dimensions to Infinity correctly represents "no constraint" until the ResizeObserver provides actual values.


47-88: LGTM!

The constructor properly validates that "display" mode requires HTMLCanvasElement and sets up the ResizeObserver with appropriate fallback for browsers without devicePixelContentBoxSize support. The non-null assertions on entries[0] and the size arrays are safe given ResizeObserver's guarantees for non-fragmented boxes.


90-104: LGTM!

Using codedWidth/codedHeight is correct for setting canvas dimensions as these represent the actual pixel buffer size. The constraint logic for "display" mode properly limits the canvas resolution to match the display area.


118-123: Verify intent: snapshot returns undefined for non-"video" modes.

The method returns undefined when canvasSize is "display" or "external", meaning snapshots are only available when the canvas matches full video resolution. This is likely intentional (to avoid low-resolution captures), but consider adding a brief JSDoc explaining why, or offering a forcedSnapshot() variant for users who accept the resolution trade-off.


136-152: LGTM!

The Options interface is well-documented with clear explanations of each canvasSize mode and their requirements. The aspect ratio warnings for "display" and "external" modes are helpful for users.

libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)

12-16: Clean class structure with proper TransformStream extension.

The class correctly extends TransformStream with typed input/output and implements the interface. The local #controller field is properly typed to match the output type.


57-98: Transform callback logic is well-structured.

The async transform correctly:

  • Guards against disposed state at entry and after awaiting
  • Accumulates configuration and frames when paused (clearing on keyframes)
  • Awaits any in-progress resume operation to prevent interleaving
  • Adds skipRendering: false to data packets matching the Output type contract

227-232: Namespace types are well-defined.

The Input and Output type aliases cleanly encapsulate the stream's data contract. The Output union correctly adds the skipRendering property to data packets while passing configuration packets through unchanged.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (11)

1-21: LGTM!

Imports are well-organized and appropriate for the streaming pipeline architecture.


23-33: LGTM!

Feature detection and capability declaration are appropriate.


35-47: LGTM!

Standard encapsulation for codec and renderer parameters.


49-59: LGTM!

Clean delegation to PauseController.


61-74: LGTM!

Size tracking delegation is clean.


76-104: LGTM!

Well-documented delegation to VideoDecoderStream for decoder metrics.


106-112: LGTM!

Timing metrics delegation is clean.


114-144: LGTM!

Renderer metrics are well-documented, including the Chrome bug reference.


202-212: LGTM!

Clean delegation to PauseController for pause/resume functionality.


214-236: LGTM!

Snapshot implementation with native fallback is well-structured. The finally block ensures proper cleanup of the temporary renderer.


250-261: LGTM!

Options interface is well-typed using Pick for WebCodecs configuration properties.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI Agents
In @libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts:
- Line 211: The dispose() method can throw if called before the microtask that
initializes this.#controller completes; guard the call by checking that
this.#controller is defined before invoking terminate(). Update dispose() to do
something like: if (this.#controller) this.#controller.terminate(); (or
null-check and no-op) so calling dispose() immediately after construction is
safe without relying on initialization timing for the controller.
- Around line 142-152: The code in resume() can access frames[frames.length - 1]
when this.#pendingFrames is empty causing a spread of undefined; add a guard at
the start (e.g., const frames = this.#pendingFrames.slice(); if (frames.length
=== 0) return;) to bail out early, then proceed with the existing loop and final
enqueue; reference this.#pendingFrames, resume(), and this.#controller.enqueue
when making the change.
- Around line 50-55: The constructor defers setting private field #controller
via "await Promise.resolve()", which can leave #controller undefined if
dispose(), resume(), or #resumeInternal() are invoked synchronously after
construction; fix by ensuring #controller is assigned synchronously in the
constructor (remove the microtask await) or, alternatively, introduce a ready
promise (e.g., this.controllerReady) that resolves when controller is set and
await that promise at the start of dispose(), resume(), and #resumeInternal() to
guard access; update references in constructor, dispose(), resume(), and
#resumeInternal() accordingly.

In @libraries/scrcpy-decoder-webcodecs/src/video/time.ts:
- Around line 21-28: The increasingNow function violates strict monotonicity by
calling nextUp(now) when now <= prevValue; change the branch so it calls
nextUp(prevValue) instead, assign that result to prevValue, and return it;
ensure you still call performance.now() to get now, compare with prevValue, and
only call nextUp(prevValue) when now <= prevValue so the returned value is
strictly greater than the prior prevValue.

In @libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts:
- Around line 71-84: The code calls this.#decoder.configure(this.#config!) after
resetting the decoder but uses a non-null assertion for the optional field
this.#config; add an explicit runtime guard before calling configure (check if
this.#config is falsy and throw a clear Error such as "Decoder not configured")
so decoding paths like the keyframe handler in video-decoder-stream.ts validate
configuration instead of relying on the protocol; keep the throw synchronous so
callers see a clear error and only call this.#decoder.configure when
this.#config is present.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (1)

114-139: Consider logging or handling pipeline errors instead of silently swallowing them.

The .catch(noop) on line 138 silently discards any errors from the streaming pipeline. While the pipeTo promise is intentionally not awaited (fire-and-forget), silently swallowing errors could make debugging difficult if the pipeline fails unexpectedly.

Based on learnings, configuration failures should propagate up rather than being recovered locally. Consider at minimum logging the error for diagnostic purposes.

🔎 Proposed improvement
         void this.#pause.readable
             .pipeTo(
                 new WritableStream({
                     write: async (packet) => {
                         // ... existing logic
                     },
                 }),
             )
-            .catch(noop);
+            .catch((error) => {
+                // Pipeline errors are expected on dispose, but log unexpected errors
+                if (error && !(error instanceof TypeError && error.message.includes("terminated"))) {
+                    console.error("TinyH264Decoder pipeline error:", error);
+                }
+            });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5bd2e and 10024a2.

📒 Files selected for processing (18)
  • libraries/adb/src/commands/subprocess/none/pty.ts
  • libraries/adb/src/commands/subprocess/none/service.ts
  • libraries/adb/src/commands/subprocess/pty.ts
  • libraries/adb/src/commands/subprocess/shell/pty.ts
  • libraries/adb/src/commands/subprocess/shell/service.ts
  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
  • libraries/scrcpy-decoder-tinyh264/src/types.ts
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/time.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
  • libraries/struct/src/utils.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-03-30T19:57:44.305Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 739
File: apps/cli/src/index.ts:163-167
Timestamp: 2025-03-30T19:57:44.305Z
Learning: The none protocol in ADB doesn't pass exit codes (always returns 0), unlike the shell protocol. The API is designed to make this distinction clear by using `exited` instead of `exit` with a code parameter for none protocol.

Applied to files:

  • libraries/adb/src/commands/subprocess/none/pty.ts
  • libraries/adb/src/commands/subprocess/pty.ts
  • libraries/adb/src/commands/subprocess/none/service.ts
📚 Learning: 2025-08-23T14:04:38.056Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 786
File: libraries/adb/src/commands/sync/sync.ts:154-157
Timestamp: 2025-08-23T14:04:38.056Z
Learning: Both ADB none protocol (`exec:`) and shell protocol (`shell:`) are executed by shell using `sh -c ...` on the Android side and require proper argument escaping. The two protocols only differ in how standard input/output are encoded for transmission, not in their execution model. The `escapeArg()` function should be used for both protocols to prevent command injection when paths contain special characters.

Applied to files:

  • libraries/adb/src/commands/subprocess/shell/service.ts
  • libraries/adb/src/commands/subprocess/shell/pty.ts
  • libraries/adb/src/commands/subprocess/none/service.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
📚 Learning: 2025-08-14T11:06:21.851Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/index.ts:1-5
Timestamp: 2025-08-14T11:06:21.851Z
Learning: The yume-chan/adb-credential-web package doesn't use "exports" in package.json for better compatibility. Instead, they re-export everything from the main entrypoint rather than using subpath exports.

Applied to files:

  • libraries/adb/src/commands/subprocess/none/service.ts
🧬 Code graph analysis (13)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPerformanceCounter (15-36)
libraries/adb/src/commands/subprocess/none/pty.ts (1)
libraries/adb/src/commands/subprocess/pty.ts (1)
  • AdbPty (8-15)
libraries/adb/src/commands/subprocess/shell/service.ts (1)
libraries/adb/src/commands/subprocess/shell/pty.ts (1)
  • AdbShellProtocolPty (20-112)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (13-15)
  • Output (22-22)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (2)
libraries/adb-scrcpy/src/video.ts (3)
  • data (76-79)
  • data (81-84)
  • data (86-100)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • CodecDecoder (7-10)
  • Config (17-20)
libraries/adb/src/commands/subprocess/shell/pty.ts (1)
libraries/adb/src/commands/subprocess/pty.ts (1)
  • AdbPty (8-15)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (4)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
  • Config (17-20)
libraries/stream-extra/src/concat.ts (1)
  • concatUint8Arrays (120-136)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (2)
  • Input (221-221)
  • Output (222-224)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • CodecDecoder (7-10)
  • Config (17-20)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
libraries/media-codec/src/av1.ts (1)
  • Av1 (197-762)
libraries/adb/src/commands/subprocess/none/service.ts (1)
libraries/adb/src/commands/subprocess/none/pty.ts (1)
  • AdbNoneProtocolPty (12-45)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)
  • PauseController (12-218)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
🪛 Biome (2.1.2)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

[error] 296-296: Unexpected constant condition.

(lint/correctness/noConstantCondition)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: diffray code review
  • GitHub Check: build (20.x)
🔇 Additional comments (40)
libraries/struct/src/utils.ts (2)

11-14: LGTM! Adding standard TextEncoder method.

The encodeInto method is part of the standard TextEncoder API and is more efficient than encode() for performance-critical paths where you can reuse buffers. The signature correctly matches the specification.


32-32: LGTM! Exposing shared encoder instance.

Making SharedEncoder public is a reasonable optimization that allows consuming code to reuse the singleton instance. Since TextEncoder is stateless, sharing the instance is safe.

libraries/adb/src/commands/subprocess/shell/service.ts (3)

5-5: LGTM! Import properly updated.

The import correctly references the renamed AdbShellProtocolPty class.


42-42: LGTM! Return type properly updated.

The return type correctly reflects the renamed class.


62-62: LGTM! Instantiation properly updated.

The instantiation correctly uses the renamed AdbShellProtocolPty class.

libraries/adb/src/commands/subprocess/none/pty.ts (2)

10-10: LGTM! Import properly updated.

The import correctly references the renamed AdbPty interface.


12-45: LGTM! Class rename with no behavioral changes.

The class rename from AdbNoneProtocolPtyProcess to AdbNoneProtocolPty is cleaner and more consistent. The implementation remains unchanged, maintaining all functionality.

libraries/adb/src/commands/subprocess/shell/pty.ts (2)

16-16: LGTM! Import properly updated.

The import correctly references the renamed AdbPty interface.


20-112: LGTM! Class rename with no behavioral changes.

The class rename from AdbShellProtocolPtyProcess to AdbShellProtocolPty is cleaner and more consistent. The implementation remains unchanged, maintaining all functionality including exit code handling, stdout streaming, and terminal control.

libraries/adb/src/commands/subprocess/none/service.ts (3)

4-4: LGTM! Import properly updated.

The import correctly references the renamed AdbNoneProtocolPty class.


45-45: LGTM! Return type properly updated.

The return type correctly reflects the renamed class.


55-55: LGTM! Instantiation properly updated.

The instantiation correctly uses the renamed AdbNoneProtocolPty class.

libraries/adb/src/commands/subprocess/pty.ts (1)

8-15: LGTM! Clean interface rename.

The interface rename from AdbPtyProcess to AdbPty is cleaner and more concise. All references across the codebase have been properly updated, including the implementing classes (AdbNoneProtocolPty and AdbShellProtocolPty) and all imports. The interface contract remains unchanged with no breaking modifications.

libraries/scrcpy-decoder-tinyh264/src/types.ts (2)

15-36: LGTM!

The renamed properties (framesRendered, framesPresented, framesSkippedRendering) provide clearer semantics, and the documentation accurately explains the distinction between rendered and presented frames, including the Chrome bug caveat.


52-52: Good flexibility with MaybePromiseLike.

Changing the return type from Promise<undefined> to MaybePromiseLike<undefined> allows implementations to return synchronously when no async work is needed, which is a good performance-conscious design choice.

libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)

3-17: LGTM!

The class correctly implements ScrcpyVideoDecoderPerformanceCounter with the renamed getters. Keeping the internal field names (#framesDrawn, #framesSkipped) while renaming only the public API is a reasonable choice that minimizes churn.

libraries/scrcpy-decoder-tinyh264/src/decoder.ts (3)

44-51: LGTM!

Clean delegation to PauseController for both the paused state and writable stream. This aligns well with the stream-based architecture.


64-90: LGTM!

The performance counter getters are properly delegated with clear documentation that matches the interface definitions.


223-225: Verify return type matches interface.

The resume() method now returns undefined synchronously, while the interface ScrcpyVideoDecoderPauseController.resume() specifies MaybePromiseLike<undefined>. This is compatible since undefined satisfies MaybePromiseLike<undefined>, but ensure this is intentional given the previous async behavior.

libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

12-15: Good design: TransformStream-based pause controller with typed namespace.

The PauseController extending TransformStream is a clean streaming-based design. The namespace exports provide well-typed Input and Output types that make the data flow explicit.

Also applies to: 220-225

libraries/scrcpy-decoder-webcodecs/src/video/time.ts (1)

3-17: LGTM!

The nextUp implementation correctly handles special cases and uses bit manipulation to compute the next representable float64 value.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)

48-49: Clarify the async Promise.resolve pattern.

The await Promise.resolve() pattern is unusual. If this is intentional to defer initialization to the next microtask, consider adding a comment explaining why.


64-69: LGTM!

Configuration handling is correct - stores the config for potential decoder resets and immediately configures the decoder.


86-89: LGTM!

Flush handling correctly waits for pending decodes before closing the decoder.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (5)

195-259: LGTM! Streaming pipeline is well-structured.

The pipeline composition correctly chains pause control, timestamp annotation, codec decoding, configuration inspection, frame decoding, and rendering. The use of increasingNow() for timestamps enables both decode time tracking and frame skipping logic.

Note: Line 259 uses catch(noop) which silently suppresses errors. This appears intentional for cleanup scenarios, but ensure critical errors are logged elsewhere if needed.


262-299: LGTM! The do-while loop is correct.

The while (true) at line 296 is used correctly in a do-while loop with an explicit break condition at line 294. This pattern ensures all queued frames are rendered in sequence, minimizing latency. The static analysis warning is a false positive.

The frame queuing strategy (lines 264-271) correctly handles backpressure by dropping intermediate frames when the renderer can't keep up.


347-357: LGTM! Options interface is appropriately simplified.

The interface correctly exposes only the relevant decoder configuration options while requiring the essential codec and renderer parameters.


320-326: LGTM!

Pause/resume methods correctly delegate to the PauseController.


179-193: LGTM!

Codec selection logic is clear and handles unsupported codecs appropriately.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (1)

7-15: LGTM! Configuration method aligns with streaming architecture.

The configure method correctly returns a CodecDecoder.Config object instead of performing side effects, which aligns with the new stream-based decoder architecture.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)

7-18: LGTM! Configuration method matches H264 pattern.

The configure method correctly returns a CodecDecoder.Config object. The comment about Microsoft Edge's explicit size requirement (lines 12-14) provides valuable context for the implementation.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)

5-8: Clean stream-based refactor with proper Annex B handling.

The class now properly extends TransformStream with the appropriate input/output types. The design cleanly separates configuration from data processing.


11-42: Transform logic is correct for H.264/H.265 Annex B format.

The implementation correctly:

  1. Stores configuration and emits the parsed config on configuration packets
  2. Concatenates configuration with the first frame data (per WebCodecs AVC/HEVC Annex B requirements)
  3. Clears the stored config after use to avoid re-concatenation
  4. Treats undefined keyframe as key (necessary for proper decoding)

The reference to the W3C WebCodecs spec in the comment is helpful for maintainability.


44-44: Abstract method signature aligns with the streaming design.

The configure method now returns CodecDecoder.Config instead of void, enabling the transformer to emit configuration downstream. This is consistent with the Output type definition.

libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)

6-9: Consistent streaming pattern with H26x decoder.

The class properly extends TransformStream with the same input/output types, maintaining consistency with the other codec implementations.


18-30: Config emission depends on sequence header presence in each packet.

The implementation correctly parses for sequence headers on every data packet and emits config when found. For AV1, sequence headers can appear in any keyframe, so this on-demand config emission is appropriate.

One consideration: if the downstream consumer requires config before processing chunks, ensure it handles the case where the first few packets might not contain a sequence header. Based on the Output type being Config | EncodedVideoChunk, downstream should be prepared to handle either.


32-39: EncodedVideoChunk creation is correct.

The keyframe handling (packet.keyframe === false ? "delta" : "key") is consistent with the H26x implementation and the comment explains the rationale clearly.

libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)

7-10: Clean type alias design for streaming decoders.

The CodecDecoder type alias cleanly defines the streaming contract. Using a namespace for associated types (Input, Config, Output) is a good TypeScript pattern for grouping related types.


25-27: Simplified constructor signature.

The parameterless constructor is cleaner and aligns with the new streaming architecture where decoders are self-contained TransformStreams.


12-22: Well-structured type definitions.

The types are well-designed:

  • Input correctly unions configuration and timestamped data packets
  • Config extends VideoDecoderConfig with required dimensions (needed for sizing)
  • Output union enables emitting both config and encoded chunks

The tsconfig includes "DOM" lib, which provides the WebCodecs API types (VideoDecoderConfig and EncodedVideoChunk) as globals, so they are properly available.

@diffray diffray bot added diffray-review-failed diffray review status: failed and removed diffray-review-started diffray review status: started labels Jan 6, 2026
@diffray diffray bot added diffray-review-started diffray review status: started and removed diffray-review-failed diffray review status: failed labels Jan 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

120-167: Non-null assertions unsafe due to constructor race condition.

Lines 142 and 157 use non-null assertions (this.#controller!) which are unsafe given the asynchronous controller initialization in the constructor. These assertions will throw if #resumeInternal is called before the microtask completes.

The iteration logic using entries() correctly handles empty arrays and properly assigns the skipRendering flag.

🤖 Fix all issues with AI Agents
In @libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts:
- Around line 52-63: The constructor's start handler defers assigning the
private field #controller via await Promise.resolve(), causing a race where
dispose(), resume(), or other methods may access an undefined #controller;
remove the unnecessary await Promise.resolve() so the assignment
this.#controller = controller happens synchronously in the start callback, and
also add a defensive guard (e.g., if (!this.#controller) return or no-op) in
dispose() and resume() to avoid non-null assertion failures when they are called
before controller is set.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10024a2 and 72c460b.

📒 Files selected for processing (4)
  • .vscode/settings.json
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/time.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
✅ Files skipped from review due to trivial changes (1)
  • .vscode/settings.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
🧬 Code graph analysis (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (13-15)
  • Output (22-22)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • frame (262-299)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build (20.x)
  • GitHub Check: diffray code review
🔇 Additional comments (9)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)

64-103: Transform logic correctly handles pause/resume flow.

The transform callback properly manages paused state by queuing configuration and data packets, and correctly awaits any in-progress resume operation before enqueueing new packets. The use of the controller parameter (not this.#controller) avoids race condition issues.


169-171: Resume signature correctly implements interface.

The return type change from MaybePromiseLike<undefined> to undefined is valid, as undefined satisfies the interface requirement for MaybePromiseLike<undefined>.


213-221: Good defensive programming with optional chaining.

Line 220 correctly uses optional chaining (this.#controller?.terminate()) to handle the case where #controller might be undefined. This resolves the past crash issue, though the root cause (constructor race condition) should still be addressed.


229-234: Clean public API definition.

The namespace exports provide a clear and well-structured public API for the PauseController, with Input and Output types that accurately reflect the streaming transformation.

libraries/scrcpy-decoder-webcodecs/src/video/time.ts (2)

1-17: Correct IEEE-754 implementation for next representable value.

The nextUp function correctly implements IEEE-754 bit manipulation to compute the next representable double-precision value. Special cases (NaN, Infinity, zero) are properly handled, and the bit arithmetic correctly increments for positive values and decrements for negative values.


21-28: Monotonicity correctly implemented.

The function now correctly calls nextUp(prevValue) (line 24) instead of nextUp(now) when now <= prevValue, ensuring strict monotonicity as required. The past review issue has been resolved.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)

46-63: Decoder initialization correctly structured.

The constructor properly initializes the VideoDecoder within the TransformStream's start callback, ensuring the decoder is ready before any transform operations. The output callback correctly increments the frames counter and enqueues frames, while the error callback propagates errors appropriately.


64-73: Config handling with appropriate guards.

Lines 65-68 correctly save the configuration and configure the decoder. The guard at lines 71-73 ensures the decoder is configured before processing chunks, with a clear error message. This addresses the protocol requirement that configuration must precede data chunks.


90-93: Proper flush and cleanup logic.

The flush callback correctly awaits pending decode operations before closing the decoder, ensuring clean shutdown of the video pipeline.

@diffray diffray bot added diffray-review-failed diffray review status: failed and removed diffray-review-started diffray review status: started labels Jan 6, 2026
@yume-chan yume-chan removed the diffray-review-failed diffray review status: failed label Jan 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

50-98: Excellent fix for the race condition!

The closure pattern elegantly solves the previous race condition by capturing controller synchronously during super() construction, then assigning it to #controller immediately after. This ensures #controller is always initialized before any methods can access it.

The transform logic correctly handles:

  • Buffering configuration and frames when paused
  • Clearing pending frames on new keyframes
  • Blocking during resume with await this.#resuming
  • Enqueuing with appropriate skipRendering flags
Optional: Add clarifying comment

Consider adding a brief comment explaining the closure pattern for future maintainers:

     constructor() {
+        // Capture controller via closure to ensure synchronous initialization
         let controller!: TransformStreamDefaultController<PauseController.Output>;
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (1)

91-94: Consider adding cleanup for abnormal stream termination.

While the flush callback properly closes the decoder, this only runs on normal stream completion. If the stream is cancelled or errors before flush completes, the decoder may remain open (though it will eventually be garbage collected).

For cleaner resource management, consider adding an abort signal or catch handler to explicitly close the decoder on abnormal termination.

💡 Optional enhancement for robust cleanup

You could capture abort/cancel by listening to the controller's abort signal or by adding error handling in the transform. However, given the current architecture and the fact that the decoder will be GC'd eventually, this is more of a nice-to-have than a must-fix.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72c460b and 95b46ba.

📒 Files selected for processing (2)
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
🧬 Code graph analysis (2)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • frame (262-299)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (13-15)
  • Output (22-22)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (10)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)

145-156: Resume logic correctly handles all cases.

The iteration using .entries() safely handles empty #pendingFrames arrays (the loop simply won't execute), and the skipRendering logic correctly marks all frames except the last one. This resolves the previous critical issue about accessing empty arrays.

The comment at lines 145-147 accurately explains the synchronization guarantee.


164-165: Disposal and return type are correct.

  • The resume() return type undefined correctly satisfies the MaybePromiseLike<undefined> interface requirement.
  • dispose() safely calls terminate() because #controller is always initialized via the constructor's closure pattern, resolving the previous crash concern.

Also applies to: 208-221


224-229: Clean type exports using namespace pattern.

The namespace pattern cleanly associates Input and Output types with the PauseController class, making the API surface clear and type-safe.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (7)

1-7: LGTM! Clean stream-based architecture.

The class properly extends TransformStream with appropriate input/output types. The union input type VideoDecoderConfig | EncodedVideoChunk elegantly handles both configuration and data packets in a single stream, aligning well with the PR's goal of converting to stream-based decoders.


8-44: Well-designed public API surface.

The exposed metrics (decodeQueueSize, framesDecoded, framesSkipped) and onDequeue event provide excellent observability into decoder behavior, which is essential for monitoring performance and latency in streaming video applications.


46-64: Proper decoder initialization and event wiring.

The initialization pattern correctly creates the VideoDecoder in the start callback and wires up output/error handlers. The error propagation on line 58 ensures configuration failures and decode errors bubble up the pipeline as expected.


65-70: Config handling looks correct.

The type guard on line 66 appropriately distinguishes VideoDecoderConfig from EncodedVideoChunk. Storing the config before calling configure() is essential for the keyframe reset logic later in the transform.


72-74: Excellent defensive coding.

The explicit state check with a clear error message ensures that encoded chunks are only processed after configuration. This validation makes the non-null assertion on line 86 safe, as the decoder can only be configured through line 68, which always sets #config first.


76-87: Smart latency management with justified non-null assertion.

The keyframe-based frame dropping strategy effectively caps latency when the decoder can't keep up. The non-null assertion on line 86 is safe because the state check on line 72 guarantees the decoder was previously configured via line 68, which always sets #config before calling configure().


89-89: LGTM!

The decode call is straightforward and safe at this point, with proper error handling wired through the decoder's error callback.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts:
- Around line 129-141: The snapshot() method lacks an explicit return type;
update its signature to declare the correct async return type (Promise<Blob |
undefined>) so callers and the compiler know it returns either a Blob or
undefined, keeping the existing logic that borrows a VideoFrameCapturerPool,
calls capturer.capture(frame), and returns that result; ensure the method
signature on the snapshot() function is changed accordingly.
- Around line 102-106: The close handler currently only closes
this.#readableController and disposes this.#counter, risking leaked VideoFrame
objects; update the close function where the ReadableStream is created (the
close: () => { ... } block) to also check and close any pending VideoFrame
references by calling close() on this.#captureFrame and this.#nextFrame if they
are non-null and then nulling them out before disposing the counter and closing
the controller so all frames are released at shutdown.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts:
- Around line 1-5: The interface uses the nonstandard property name `writeable`;
change `VideoFrameRenderer.writeable` to `writable` to match the standard stream
API and the `RendererController` usage in `flow-control.ts`, then update all
implementations and references to `VideoFrameRenderer` (including any
classes/functions that consume or construct that interface) to use `writable` so
names are consistent across the codebase.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 14-60: createProgram may receive null from gl.createProgram() if
the WebGL context is lost; add an immediate null check after const program =
gl.createProgram() in createProgram and handle it before calling
gl.attachShader/linkProgram. Specifically, if program is null, delete the
vertexShader and fragmentShader (to avoid leaks) and throw a clear Error (e.g.,
"Failed to create program: WebGL context lost") so subsequent
gl.attachShader/gl.linkProgram calls never run on a null program.

In @libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts:
- Around line 13-16: The explicit cast "(this.#canvas as HTMLCanvasElement)" is
unnecessary because both OffscreenCanvas and HTMLCanvasElement support
getContext("bitmaprenderer"); remove the cast and call
this.#canvas.getContext("bitmaprenderer", { alpha: false })! to preserve type
safety; if TypeScript still complains about the union type, either add a short
comment explaining why a cast is required or narrow the type first (e.g., check
instanceof HTMLCanvasElement or assert via a single localized cast) while
keeping the comment explaining the reason.
🧹 Nitpick comments (5)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (1)

18-30: Consider caching to avoid redundant sequence header parsing.

The code instantiates a new Av1 parser and searches for sequence headers on every packet. Since AV1 sequence headers typically appear only once at stream start, this creates unnecessary overhead for the majority of packets that won't contain sequence headers.

Consider tracking whether a configuration has already been emitted and skipping the parsing for subsequent packets, unless dynamic resolution changes are expected.

♻️ Potential optimization approach
+    private configSent = false;
+
     constructor() {
         super({
             transform: (packet, controller) => {
                 if (packet.type === "configuration") {
                     // AV1 decoder doesn't need configuration packets
                     return;
                 }
 
-                const parser = new Av1(packet.data);
-                const sequenceHeader = parser.searchSequenceHeaderObu();
-
-                if (sequenceHeader) {
+                if (!this.configSent) {
+                    const parser = new Av1(packet.data);
+                    const sequenceHeader = parser.searchSequenceHeaderObu();
+
+                    if (sequenceHeader) {
-                    const width = sequenceHeader.max_frame_width_minus_1 + 1;
-                    const height = sequenceHeader.max_frame_height_minus_1 + 1;
-
-                    controller.enqueue({
-                        codec: Av1.toCodecString(sequenceHeader),
-                        codedWidth: width,
-                        codedHeight: height,
-                    });
+                        const width = sequenceHeader.max_frame_width_minus_1 + 1;
+                        const height = sequenceHeader.max_frame_height_minus_1 + 1;
+
+                        controller.enqueue({
+                            codec: Av1.toCodecString(sequenceHeader),
+                            codedWidth: width,
+                            codedHeight: height,
+                        });
+                        this.configSent = true;
+                    }
                 }
 
                 controller.enqueue(

Note: If the stream supports dynamic resolution changes, you may want to check for sequence headers periodically rather than only once.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)

13-15: Consider documenting the pool capacity choice.

The pool capacity of 4 appears to be a magic number. Consider adding a comment explaining the rationale for this size (e.g., expected concurrent snapshot operations, memory constraints, etc.).

📝 Suggested documentation improvement
+// Pool size of 4 allows up to 4 concurrent snapshot operations
+// while keeping memory usage bounded
 const VideoFrameCapturerPool =
     /* #__PURE__ */
     new Pool(() => new VideoFrameCapturer(), 4);
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (1)

11-14: Consider aligning bitmaprenderer context configurations across renderers.

The alpha: true configuration in bitmap.ts follows MDN best practices to avoid the performance cost of alpha: false. However, snapshot.ts (line 15) still uses alpha: false for its ImageBitmapRenderingContext in the VideoFrameCapturer class. While the use cases differ (rendering vs. capturing), inconsistent configurations can create confusion. Either align both to use alpha: true following the MDN guidance, or document why snapshot.ts requires alpha: false.

libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (1)

24-26: Consider renaming getter to match standard spelling.

The getter is named writeable but exposes writable from the generator. The standard spelling is "writable" (as used in the Web Streams API and line 25). Consider renaming for consistency.

📝 Suggested rename
-    get writeable() {
+    get writable() {
         return this.#generator.writable;
     }
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

19-35: Frame lifecycle looks correct, but consider getter naming.

The stream implementation properly manages frame lifecycle by closing the previous frame before saving the new one. The close/abort handlers correctly call dispose().

However, the getter is named writeable (non-standard spelling) instead of writable (standard Web Streams API spelling). This inconsistency also appears in insertable-stream.ts.

📝 Suggested rename for consistency
-    get writeable() {
+    get writable() {
         return this.#writable;
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95b46ba and bf514f2.

📒 Files selected for processing (16)
  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
  • libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/utils.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
💤 Files with no reviewable changes (2)
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/utils.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (8)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
libraries/stream-extra/src/consumable.ts (1)
  • WritableStream (71-71)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (4)
libraries/scrcpy-decoder-webcodecs/src/video/pool.ts (1)
  • Pool (1-37)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPerformanceCounter (15-36)
libraries/stream-extra/src/push-readable.ts (2)
  • PushReadableStreamController (6-14)
  • PushReadableStream (41-307)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (2)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (1)
  • canvas (40-42)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)
  • canvas (8-10)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
libraries/media-codec/src/av1.ts (1)
  • Av1 (197-762)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (4)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
  • Config (17-20)
libraries/stream-extra/src/concat.ts (1)
  • concatUint8Arrays (120-136)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (2)
  • glCreateContext (11-59)
  • glLoseContext (61-67)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • Options (205-212)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (1)
  • createCanvas (1-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (38)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)

1-4: LGTM!

The imports are clean and correctly reference the necessary dependencies for the stream-based codec implementation.


6-9: LGTM!

The class declaration correctly extends TransformStream and implements CodecDecoder, aligning with the PR's objective to convert decoders to a stream-based architecture.


32-39: LGTM!

The EncodedVideoChunk generation correctly handles the keyframe fallback—treating undefined as "key" is a safe default that ensures decodability. The packet data and timestamp are properly forwarded to the chunk.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (4)

1-1: LGTM!

The new imports are properly used: concatUint8Arrays for merging configuration with frame data (line 26), and TransformStream for the stream-based decoder architecture (line 6).


5-8: LGTM!

The refactor to extend TransformStream<CodecDecoder.Input, CodecDecoder.Output> correctly aligns the decoder with the stream-based architecture, replacing the previous direct VideoDecoder approach.


11-42: LGTM!

The transform logic correctly implements the stream-based decoding:

  • Configuration packets are stored and the configure() result is enqueued for downstream processing
  • Data packets properly handle Annex B format by concatenating configuration with the first frame (lines 25-27), then clearing the config
  • The keyframe type derivation (line 35) correctly treats undefined as "key" to ensure frames decode properly

The absence of error handling allows configuration failures to propagate up the client pipeline, which aligns with the intended error handling strategy.

Based on learnings, configuration failures should propagate to the client pipeline for proper error handling.


44-44: All subclasses have been updated to implement the new signature. Both H264Decoder and H265Decoder correctly override configure() with the return type CodecDecoder.Config, and the return values are properly enqueued into the stream.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (2)

1-44: LGTM: Well-structured stream wrapper with clean metrics API.

The class structure is sound. The definite assignment assertion on line 8 is necessary because the field is assigned after the super() call. The public getters provide a clean observability surface.


46-103: LGTM: Stream implementation correctly handles decoder lifecycle.

The constructor properly implements the TransformStream pattern:

  • Start handler initializes the decoder with appropriate callbacks
  • Transform handler validates configuration state before decoding
  • Flush handler correctly awaits pending frames before closing
  • Cancel handler immediately closes on error

The implementation follows best practices for WebCodecs decoder management.

libraries/scrcpy-decoder-tinyh264/src/decoder.ts (4)

26-28: LGTM: Appropriate noop export for error handling.

Exporting the noop function supports the streaming pattern used throughout the refactoring, particularly for .catch(noop) on void promises.


44-90: LGTM: Improved metric naming and documentation.

The refactoring to use PauseController and PerformanceCounter improves the public API surface. The renamed metrics (framesRendered, framesSkippedRendering) better distinguish rendering from decoding stages, and the documentation clearly explains frame presentation semantics including the Chrome bug caveat.


114-138: LGTM: Streaming pipeline correctly handles TinyH264 constraints.

The refactored pipeline properly:

  • Validates decoder configuration before processing data packets
  • Documents the TinyH264 limitation regarding frame metadata and skipping
  • Copies packet data to avoid BufferCombiner issues
  • Uses the standard void/catch(noop) pattern for fire-and-forget error handling

208-208: No action required. The PerformanceCounter class correctly provides the increaseFramesRendered() method at line 54 in libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts, which increments the #framesDrawn counter. The method is properly used at line 208 in the decoder.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (5)

42-57: LGTM: Clean pause controller with timestamp-based frame skipping.

The PauseController integration and #skipFramesUntil mechanism provide a robust approach to handling frame skipping during pause/resume cycles using monotonic timestamps.


74-115: LGTM: Well-documented decode time tracking rationale.

The delegation to VideoDecoderStream is clean, and the comment at lines 104-108 clearly explains why totalDecodeTime is tracked at this level rather than in the stream—it includes pre-processing time and relies on timestamps set by this class.


117-147: LGTM: Comprehensive rendering metrics with clear documentation.

The RendererController integration provides good observability. The documentation on framesPresented (lines 127-135) effectively explains the latency optimization and documents the Chrome nested Worker limitation.


152-176: LGTM: Clean codec selection and well-typed Options interface.

The constructor properly instantiates codec-specific decoders with appropriate defaults. The Options interface cleanly extends relevant VideoDecoderConfig properties, and the default values (hardwareAcceleration = "no-preference", optimizeForLatency = true) align well with low-latency streaming requirements.

Also applies to: 269-279


177-240: No action required. Line 239 correctly uses renderer.writeable, which matches the VideoFrameRenderer interface definition in type.ts. While the standard Web Streams API uses writable, this codebase intentionally defines and consistently uses writeable as the property name across all VideoFrameRenderer implementations (CanvasVideoFrameRenderer, InsertableStreamVideoFrameRenderer).

Likely an incorrect or invalid review comment.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (4)

78-101: LGTM! Well-designed frame queuing with proper lifecycle management.

The write logic correctly handles:

  • Cloning frames for snapshot capture (Line 82) to avoid ownership conflicts
  • Skipping intermediate frames (Lines 88-91) to minimize latency
  • Fire-and-forget drawing (Line 100) to avoid blocking the writable stream

This design aligns with the official Scrcpy client's low-latency approach.


109-127: LGTM! Proper backpressure handling with frame draining.

The #draw method correctly:

  • Drains all queued frames in a loop (Lines 120-124)
  • Respects backpressure via await on enqueue (Line 122)
  • Updates performance metrics (Line 123)

The comments on Lines 112-117 provide excellent context for the latency-focused design choice.


71-77: LGTM! Proper abort signal wiring.

The writable's start handler correctly propagates cancellation from the readable stream to the writable stream (Lines 74-75), ensuring proper cleanup when the consumer cancels.


38-64: LGTM! Clean delegation to PerformanceCounter.

The performance counter getters correctly delegate to the internal PerformanceCounter instance with comprehensive JSDoc documentation that matches the interface contract.

libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts (1)

5-5: LGTM! Properly exports the new flow-control module.

The export is consistent with the existing pattern and makes the RendererController class available as part of the public render API.

libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (1)

6-7: Reference the HTML spec clarifying ImageBitmap width/height behavior.

The HTML specification explicitly states that transferFromImageBitmap() makes the context's output bitmap refer to the provided ImageBitmap, with its intrinsic size derived from the ImageBitmap itself—not the canvas dimensions. ImageBitmap width/height can differ from canvas width/height; that's the intended behavior. Update the comment to reflect this, or clarify if there's a different reason options is not supported.

libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (3)

3-3: LGTM!

The type import for WritableStream is correctly added to support the new stream-based API.


29-31: LGTM!

The stream getter appropriately exposes the internal MediaStream for external use.


49-52: LGTM!

The resize handler correctly synchronizes element dimensions with video frame dimensions, maintaining proper aspect ratio.

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (6)

2-2: LGTM!

The WritableStream import is correctly added to support the new stream-based rendering pipeline.


12-17: LGTM!

The new private fields appropriately support the device-pixel rendering feature and frame lifecycle management.


37-77: Verify error handling for concurrent draw calls.

The useDevicePixels feature is well-implemented with proper runtime validation. However, on line 72, void this.draw(this.#frame) discards the promise, which could lead to unhandled rejections if draw() fails. Additionally, rapid resize events might trigger overlapping draw calls.

Consider whether the draw operation should be awaited or if there should be debouncing/throttling for resize events.


79-92: LGTM!

The #updateSize method correctly clamps dimensions to the frame size and avoids unnecessary updates when dimensions haven't changed.


105-122: LGTM!

The Options interface is well-documented with clear guidance on the useDevicePixels feature, including performance implications and usage constraints.


96-102: No breaking changes from the return type modification.

The dispose() method is not part of the VideoFrameRenderer interface contract, nor does it implement the standard Disposable interface which would require dispose(): void. The current callers at lines 30-31 (stream handler callbacks) do not await the method, so the return type change to undefined introduces no compatibility issues.

The cleanup logic properly disconnects the ResizeObserver and closes the saved frame.

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (5)

1-2: LGTM!

The cspell directives correctly suppress false positives for GLSL precision qualifiers.


95-96: Verify context type narrowing is intentional.

Line 95 narrows the context type to WebGLRenderingContext, but glCreateContext (line 111) can return WebGL2RenderingContext. While the instanceof check on line 161 handles WebGL2 features at runtime, the type narrowing loses compile-time type safety for WebGL2-specific APIs.

Consider using a union type to preserve type information:

#context: WebGLRenderingContext | WebGL2RenderingContext;

105-152: LGTM!

The constructor properly integrates the options parameter, passing it to the parent class and using enableCapture to configure WebGL context attributes. The trade-off between preserveDrawingBuffer and desynchronized is well-balanced for capture vs. latency.

The program creation uses the new helper function and is properly initialized with useProgram.


169-191: LGTM!

The override keyword correctly indicates method inheritance. The addition of gl.flush() ensures commands are submitted for execution, which can improve frame timing in streaming scenarios, especially with the desynchronized context setting.


193-213: LGTM!

The disposal sequence correctly cleans up resources in the proper order: program deletion, context invalidation, then parent cleanup. The Options interface appropriately extends the parent options and adds the enableCapture flag with clear documentation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (1)

13-62: Missing cleanup logic for MediaStreamTrackGenerator.

The class no longer has a dispose() method after removing the lifecycle methods. The MediaStreamTrackGenerator track should be stopped when no longer needed to release resources. Consider adding cleanup:

🔧 Suggested addition
+    dispose(): undefined {
+        this.#generator.stop();
+        this.#stream.getTracks().forEach(track => track.stop());
+    }
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

212-223: Missing controller.terminate() in dispose.

The dispose() method clears internal state but doesn't terminate the TransformStream. Downstream consumers may hang waiting for more data. Consider terminating the controller:

🔧 Suggested fix
     dispose() {
         if (this.#disposed) {
             return;
         }

         this.#disposed = true;

         this.#pendingConfiguration = undefined;
         this.#pendingFrames.length = 0;

         this.#disposeVisibilityTracker?.();
+        this.#controller.terminate();
     }
🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts:
- Around line 220-234: The TransformStream transform callback filters out frames
when frame.timestamp <= this.#skipFramesUntil but never closes them, leaking GPU
resources; update the transform function inside the TransformStream in
decoder.ts to call frame.close() for skipped VideoFrame instances (i.e., when
frame.timestamp <= this.#skipFramesUntil) before returning, and keep
controller.enqueue(frame) only for frames that are forwarded so that all skipped
frames are explicitly closed.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts:
- Around line 108-113: The abort() handler currently leaves #nextFrame open
which can keep a pending VideoFrame alive; after calling
this.#readableController.error(reason) and this.#counter.dispose() also
explicitly close/cancel #nextFrame to release any held VideoFrame and avoid
leaks (but keep the existing behavior of not closing #captureFrame so snapshots
still work); make the close/cancel call defensive (check for existence and
whether it's already closed) so it’s safe to call from abort and won’t interfere
with normal #draw() completion.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

56-76: Consider error handling for resize redraw.

The void this.draw() on line 72 swallows any errors from the redraw operation. While this prevents unhandled promise rejections, errors during resize-triggered redraws will be silently lost.

💡 Optional: Add error logging
                 if (this.#updateSize(width, height) && this.#frame) {
-                    void this.draw(this.#frame);
+                    void this.draw(this.#frame).catch((e) => {
+                        console.error("Failed to redraw on resize:", e);
+                    });
                 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf514f2 and 80458c1.

📒 Files selected for processing (7)
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (3)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (8)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)
  • PauseController (12-224)
  • Output (228-230)
  • Input (227-227)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (2)
  • RendererController (17-159)
  • frame (117-136)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Output (22-22)
  • Input (13-15)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (1)
  • H264Decoder (6-16)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)
  • H265Decoder (6-19)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (1)
  • Av1Codec (6-43)
libraries/scrcpy-decoder-webcodecs/src/video/time.ts (1)
  • increasingNow (21-28)
libraries/stream-extra/src/inspect.ts (1)
  • InspectStream (5-23)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
libraries/stream-extra/src/consumable.ts (1)
  • WritableStream (71-71)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (7)
libraries/scrcpy-decoder-webcodecs/src/video/pool.ts (1)
  • Pool (1-37)
libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts (1)
  • VideoFrameCapturer (1-42)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPerformanceCounter (15-36)
libraries/stream-extra/src/push-readable.ts (2)
  • PushReadableStreamController (6-14)
  • PushReadableStream (41-307)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (11)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)

1-5: Clean stream-based interface design.

The simplified interface exposing only writable: WritableStream<VideoFrame> aligns well with the stream-based architecture and matches the standard TransformStream.writable naming convention.

libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (1)

49-52: Good use of resize event for dimension synchronization.

The resize event listener correctly syncs the video element's width/height attributes with its intrinsic videoWidth/videoHeight, ensuring proper rendering dimensions.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (2)

152-158: Proper cleanup in dispose.

The dispose() method correctly closes both #captureFrame and #nextFrame, preventing resource leaks when manually cleaning up.


78-94: Frame lifecycle is correct; no use-after-close issue.

The write handler intentionally stores the original frame in #nextFrame (line 92) without cloning because frame ownership transfers to the stream via enqueue() in the #draw method. The #nextFrame reference is cleared to undefined (line 129) before the frame is enqueued, so when subsequent writes arrive during drawing, they do not close an in-flight frame—they only replace the queued frame reference. The renderer receives ownership of the enqueued frame and is responsible for closing it. The pattern is sound: #captureFrame is cloned for snapshot safety, while #nextFrame maintains the original reference that transfers ownership downstream.

libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (2)

50-102: Clean synchronous controller initialization.

The controller capture pattern correctly assigns the controller synchronously in the start callback, avoiding the race condition from the previous implementation that used await Promise.resolve().


149-162: Empty array case handled correctly.

The for...of iteration over entries() safely handles an empty #pendingFrames array by simply not executing the loop body, avoiding the previous concern about accessing frames[-1].

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)

19-32: Good stream-based frame handling.

The WritableStream properly manages the frame lifecycle: closes the previous frame before storing the new one, conditionally updates size based on options, and delegates to the abstract draw() method.


79-92: Well-designed size update logic.

The #updateSize method correctly clamps dimensions to avoid upscaling beyond source resolution and returns a boolean to enable conditional redraws, avoiding unnecessary work.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (3)

177-240: Well-structured streaming pipeline.

The pipeline composition is clean and readable with clear stage responsibilities:

  1. Timestamp injection
  2. Codec-specific packet transformation
  3. Configuration inspection/size tracking
  4. Raw video decoding
  5. Decode time tracking and skip filtering
  6. Render flow control
  7. Final rendering

The void ... .catch(noop) pattern prevents unhandled rejections.


270-281: Clean options interface using type reuse.

Using Pick<VideoDecoderConfig, ...> ensures type consistency with the native WebCodecs API and avoids duplicating type definitions.


259-267: dispose() effectively halts pipeline processing through the #disposed flag.

The dispose() method doesn't need to explicitly abort the writable stream. When #pause.dispose() is called, it sets #disposed = true, which causes PauseController's transform function to return early without enqueueing packets (lines 58-59 and 83-84). This prevents any further data from flowing through the pipeline, even if the source stream continues sending data.

The design is intentional: dispose() handles resource cleanup while stream closure is managed separately via the comment's note about cleanup happening "when writable ends."

Likely an incorrect or invalid review comment.

@yume-chan yume-chan changed the title refactor(webcodecs): convert decoder to be stream based feat(webcodecs): drop decoding/rendering frames when decoder/renderer can't keep up Jan 9, 2026
@yume-chan yume-chan changed the title feat(webcodecs): drop decoding/rendering frames when decoder/renderer can't keep up feat(webcodecs): skip decoding/rendering frames when decoder/renderer can't keep up Jan 9, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts:
- Around line 13-15: The comment next to the alpha: true setting in bitmap.ts
incorrectly cites WebGL guidance; update it to reference
bitmaprenderer/ImageBitmapRenderingContext guidance (e.g., MDN
ImageBitmapRenderingContext or canvas getContext options) and clarify why we
keep alpha: true here: if transparency is required we must use alpha: true,
whereas setting alpha: false can enable opaque-canvas optimizations for
bitmaprenderer — replace the WebGL link and/or add a short note explaining that
distinction next to the alpha: true setting.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)

248-250: Consider simplifying the return type.

The explicit undefined return type and return statement is unusual. Consider using void return type instead:

♻️ Proposed simplification
-    resume(): undefined {
+    resume(): void {
         this.#pause.resume();
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80458c1 and e0897c3.

📒 Files selected for processing (3)
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (1)
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (5)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)
  • canvas (8-10)
  • Options (113-128)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (2)
  • canvas (40-42)
  • Options (264-271)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • Options (272-282)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • Options (205-212)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)
  • frame (117-136)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (6)
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (2)

6-10: LGTM!

Constructor signature update aligns with the Options-based pattern used in CanvasVideoFrameRenderer and WebGLVideoFrameRenderer.


19-23: The review comment is incorrect. The frame parameter should NOT be closed in the draw() method.

The parent class CanvasVideoFrameRenderer explicitly documents (line 99-100 in canvas.ts):

"Derived classes must not transfer or close the frame."

The frame is intentionally kept alive by CanvasVideoFrameRenderer to support redraw on resize (stored in #frame field). The parent class manages the frame lifecycle by:

  • Closing the previous frame when a new one arrives (line 21-22)
  • Closing it when the stream closes or is disposed (line 108)

The comment in flow-control.ts about "the renderer is responsible for closing frame" refers to the CanvasVideoFrameRenderer as the owner of frame lifecycle, not the individual draw() method implementation. No fix is needed.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (4)

177-241: Verify error handling strategy in the streaming pipeline.

The .catch(noop) at line 241 silently swallows all pipeline errors, including configuration failures, decoder errors, and renderer failures. Based on learnings, configuration failures should propagate up the client pipeline rather than being suppressed.

Consider whether errors should be:

  1. Logged for debugging
  2. Propagated through an error callback/event
  3. Exposed via a promise that clients can monitor

This is especially important for configuration errors during mid-stream encoder restarts.


260-268: LGTM: Dispose method aligns with streaming lifecycle.

The dispose implementation correctly relies on automatic stream cleanup when the writable ends, with explicit cleanup only for resources that persist beyond the stream lifecycle (size tracking, pause controller, render controller).


272-275: LGTM: Clean interface design.

The Options interface appropriately extends VideoDecoderConfig using Pick to inherit the relevant hardware acceleration and latency optimization settings, which are then applied to the config in the pipeline (lines 210-211).


190-190: No timing issue: increasingNow() is built directly on performance.now() and ensures monotonic values while preserving the same time base. The subtraction at line 225 (performance.now() - frame.timestamp) produces correct decode time calculations.

Likely an incorrect or invalid review comment.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 9, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts:
- Around line 177-241: The final .pipeTo(renderer.writable).catch(noop) silently
swallows all pipeline errors; remove the noop swallow and instead propagate or
surface errors: replace the trailing .catch(noop) with either returning the
Promise from pipeTo so callers can await it, or attach a handler that logs and
re-throws (e.g., .catch(err => { processError(err); throw err; })), so failures
from the stream chain (starting from this.#pause through codecDecoder,
InspectStream, this.#rawDecoder, this.#renderController to renderer.writable)
are not suppressed; ensure whichever method you pick surfaces errors to the
consumer or options-provided error callback.
- Around line 104-115: The code is repurposing
EncodedVideoChunk.timestamp/VideoFrame.timestamp to store local processing time
which violates WebCodecs semantics; stop writing processing milliseconds into
EncodedVideoChunk.timestamp and instead track per-frame processing times using a
separate structure (e.g., a WeakMap from VideoFrame/EncodedVideoChunk to timing
or an internal field on your decoder instance), update any logic that reads
timestamps to use the new timing storage, and keep the existing get
totalDecodeTime() (#totalDecodeTime) aggregate unchanged so presentation
timestamps remain untouched and in microseconds.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/fallback.ts:
- Around line 6-26: The constructor of AutoRenderer currently types the options
parameter as WebGLVideoFrameRenderer.Options but passes it to both
WebGLVideoFrameRenderer and BitmapVideoFrameRenderer; change the constructor
signature in AutoRenderer to accept options: CanvasVideoFrameRenderer.Options so
the type matches what both renderers accept (WebGLVideoFrameRenderer will still
accept the base options since it extends them); update the constructor parameter
type only (constructor(canvas?: HTMLCanvasElement | OffscreenCanvas, options?:
CanvasVideoFrameRenderer.Options)) and leave the existing logic that
instantiates WebGLVideoFrameRenderer or BitmapVideoFrameRenderer unchanged.
🧹 Nitpick comments (3)
libraries/scrcpy-decoder-webcodecs/src/video/render/fallback.ts (1)

28-30: Use optional chaining for dispose call.

Since dispose is optional in the VideoFrameRenderer interface (line 7 in type.ts), calling this.#inner.dispose() directly could throw if the inner renderer doesn't implement it.

🛡️ Proposed fix using optional chaining
 dispose() {
-    return this.#inner.dispose();
+    return this.#inner.dispose?.();
 }
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (2)

177-241: Consider extracting pipeline construction to helper methods.

The constructor contains a deeply nested pipeline with multiple transformation stages spanning 60+ lines. Extracting logical stages into private helper methods would improve readability and testability.

💡 Example refactoring approach
constructor(options: WebCodecsVideoDecoder.Options) {
    // ... initialization ...
    
    void this.#pause.readable
        .pipeThrough(this.#createTimestampTransform())
        .pipeThrough(codecDecoder)
        .pipeThrough(this.#createConfigInspector(hardwareAcceleration, optimizeForLatency))
        .pipeThrough(this.#rawDecoder)
        .pipeThrough(this.#createDecodeTimeTracker())
        .pipeThrough(this.#renderController)
        .pipeTo(renderer.writable)
        .catch(noop);
}

#createTimestampTransform() {
    return new TransformStream<PauseController.Output, CodecDecoder.Input>({
        transform: (packet, controller) => {
            // ... existing logic ...
        },
    });
}

#createConfigInspector(hardwareAcceleration: string, optimizeForLatency: boolean) {
    return new InspectStream((chunk): undefined => {
        // ... existing logic ...
    });
}

#createDecodeTimeTracker() {
    return new TransformStream<VideoFrame, VideoFrame>({
        transform: (frame, controller) => {
            // ... existing logic ...
        },
    });
}

248-250: Unusual explicit undefined return type.

The resume() method explicitly returns undefined rather than void. While this matches TypeScript's behavior (void functions return undefined at runtime), the conventional practice is to use void for methods without meaningful return values. This likely matches the ScrcpyVideoDecoder interface signature.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0897c3 and 46383d1.

📒 Files selected for processing (4)
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/fallback.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (1)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (6)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)
  • PauseController (12-224)
  • Output (228-230)
  • Input (227-227)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (1)
  • VideoDecoderStream (4-104)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (2)
  • RendererController (17-159)
  • frame (117-136)
libraries/scrcpy-decoder-webcodecs/src/video/render/fallback.ts (1)
  • AutoRenderer (6-31)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Output (22-22)
  • Input (13-15)
libraries/scrcpy-decoder-webcodecs/src/video/time.ts (1)
  • increasingNow (21-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (8)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)

4-8: LGTM! Clean stream-based API.

The transition to a WritableStream<VideoFrame> property provides a cleaner, more composable API compared to the previous imperative method-based approach. Making dispose optional is reasonable for flexibility.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (7)

42-57: Well-designed pause control mechanism.

The pause controller setup with #skipFramesUntil provides a clean mechanism for coordinating frame skipping across the pipeline. The timestamp-based approach correctly marks the boundary between frames to skip and frames to process.


74-102: Clean delegation to VideoDecoderStream.

The raw decoder setup correctly exposes VideoDecoderStream metrics through well-documented getters. The naming convention (e.g., framesSkippedDecoding vs framesSkippedRendering) clearly distinguishes between different pipeline stages.


117-147: Comprehensive renderer metrics with helpful documentation.

The renderer controller setup provides clear metrics tracking with excellent JSDoc. The note about the Chrome bug for framesPresented in nested Web Workers is particularly valuable for debugging potential issues.


256-258: Clean delegation for snapshot functionality.

The snapshot() method correctly delegates to the render controller, allowing callers to capture the current rendered frame state. Errors propagate naturally to the caller.


273-283: Type-safe Options interface using Pick.

The Options interface correctly uses Pick<VideoDecoderConfig> to ensure type compatibility with the WebCodecs API. This prevents type drift if the VideoDecoderConfig interface changes in the future.


6-6: No action required.

The imports of PauseController and noop from @yume-chan/scrcpy-decoder-tinyh264 are appropriate. PauseController is a decoder-specific abstraction that handles pause/resume logic, pending frame buffering, and document visibility tracking for video decoding—not a general utility. The webcodecs decoder intentionally builds on this tinyh264 abstraction, and no circular dependency exists (tinyh264 does not depend on webcodecs). The current architecture is sound.

Likely an incorrect or invalid review comment.


260-269: No changes needed. The code correctly handles #rawDecoder cleanup through the stream pipeline lifecycle.

When renderer.writable ends (either through normal completion or error), the pipeTo() operation automatically cancels all upstream transforms in the chain, including #rawDecoder. This invokes VideoDecoderStream.cancel(), which calls decoder.close(). The comment explaining that "cleanup happens automatically when writable ends" is accurate and the current implementation is correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (1)

74-76: Consider explicit event listener cleanup.

The "dequeue" event listener is never explicitly removed. While the decoder being closed prevents further events, using an AbortController or storing the listener reference for explicit removal would be cleaner.

♻️ Optional: Use AbortController for cleanup
+    #dequeueAbortController = new AbortController();
+
     constructor() {
         let decoder!: VideoDecoder;
 
         super({
             start: (controller) => {
                 decoder = new VideoDecoder({
                     output: (frame) => {
                         this.#framesDecoded += 1;
                         controller.enqueue(frame);
                     },
                     error: (error) => {
                         controller.error(error);
                         this.#onDequeue.dispose();
+                        this.#dequeueAbortController.abort();
                     },
                 });
-                decoder.addEventListener("dequeue", () =>
-                    this.#onDequeue.fire(undefined),
+                decoder.addEventListener(
+                    "dequeue",
+                    () => this.#onDequeue.fire(undefined),
+                    { signal: this.#dequeueAbortController.signal },
                 );
             },
             // ... in flush and cancel:
+            // this.#dequeueAbortController.abort();
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3b8d12 and 8e8240d.

📒 Files selected for processing (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
🧬 Code graph analysis (1)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (7)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (7)

4-7: Clean API design with dual-purpose input type.

Using a union type for configuration and encoded chunks allows a single stream to handle both setup and data flow elegantly.


8-27: Well-documented state invariants.

The documentation clearly explains why the callbacks don't need to check for "closed" state, making the code easier to maintain. The definite assignment pattern is safe since start runs synchronously.


29-35: Good encapsulation of event emission.

Exposing only the event property prevents external code from firing events while still allowing subscriptions.


37-51: Good observability metrics.

These counters provide useful visibility into decoder performance and latency management behavior.


53-56: LGTM!

The saved configuration enables proper decoder reset behavior.


78-104: Solid latency management strategy.

The keyframe-based reset logic effectively bounds latency by discarding pending frames when the decoder can't keep up. The comment clearly explains the rationale. The non-null assertion on #config at line 100 is safe since the unconfigured state check on line 85 guarantees prior configuration.


105-124: Proper shutdown handling for both graceful and abrupt termination.

The flush callback correctly awaits pending decodes for graceful shutdown, while cancel immediately closes the decoder for error scenarios. The state check before flushing handles the edge case where no configuration was ever sent.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 12, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 199-200: Typo in the inline comment: change "clsoed" to "closed"
above the assignment to the private field this.#lastFrame in webgl.ts; update
the comment to read something like "// Will be closed by
`CanvasVideoFrameRenderer`" next to the this.#lastFrame = frame; assignment to
fix spelling and clarify intent referencing CanvasVideoFrameRenderer and the
#lastFrame field.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

79-81: Potential unhandled promise rejection in resize callback.

The void this.draw(...) call discards the promise result. If draw() throws, the error will become an unhandled rejection. Consider wrapping in a .catch() handler.

Suggested fix
                 if (this.#updateSize(width, height) && this.#lastFrame) {
-                    void this.draw(this.#lastFrame);
+                    void this.draw(this.#lastFrame).catch(() => {
+                        // Ignore errors during resize redraw
+                    });
                 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e8240d and 2fd963c.

📒 Files selected for processing (6)
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
📚 Learning: 2025-08-14T11:06:21.851Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/index.ts:1-5
Timestamp: 2025-08-14T11:06:21.851Z
Learning: The yume-chan/adb-credential-web package doesn't use "exports" in package.json for better compatibility. Instead, they re-export everything from the main entrypoint rather than using subpath exports.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (3)
libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts (5)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
  • VideoFrameRenderer (4-8)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (3)
  • canvas (11-13)
  • options (16-18)
  • Options (119-134)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (2)
  • WebGLVideoFrameRenderer (62-247)
  • Options (250-257)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • Options (227-237)
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (1)
  • BitmapVideoFrameRenderer (3-24)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (10)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)
  • PauseController (12-224)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (1)
  • VideoDecoderStream (4-136)
libraries/scrcpy-decoder-webcodecs/src/video/utils/timestamp.ts (1)
  • TimestampTransforms (35-99)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)
  • RendererController (17-159)
libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts (1)
  • AutoCanvasRenderer (6-31)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)
  • Options (119-134)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • Options (250-257)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (1)
  • H264TransformStream (6-16)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)
  • H265TransformStream (6-19)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (1)
  • Av1TransformStream (6-46)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (2)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)
  • Options (119-134)
  • canvas (11-13)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (2)
  • glCreateContext (11-59)
  • glLoseContext (61-67)
🔇 Additional comments (16)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (4)

25-40: WritableStream lifecycle looks correct.

The write callback properly closes the previous frame before storing the new one, and close/abort both call dispose() which handles cleanup of the ResizeObserver and last frame. This aligns with the learning about explicit cleanup in stream-based code paths.


64-84: Non-null assertions on ResizeObserverEntry arrays.

The code uses non-null assertions (!) on entries[0], entry.devicePixelContentBoxSize[0], and entry.contentBoxSize[0]. While these should always be populated for a ResizeObserver callback, the defensive pattern would be to guard against empty arrays.

However, given the ResizeObserver spec guarantees at least one entry and one size box for observed elements, these assertions are safe in practice.


87-100: #updateSize clamps to frame dimensions correctly.

The method properly clamps the canvas size to the frame's coded dimensions when a frame is present, preventing the canvas from being larger than the video content. The boolean return value enables callers to know if a redraw is needed.


109-115: dispose() cleanup is thorough.

The method properly disconnects the ResizeObserver, closes the last frame, and resets canvas dimensions. This explicit cleanup aligns with the retrieved learning about not relying on stream cancel callbacks for cleanup.

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (4)

14-19: createShader lacks compilation error checking.

The helper creates and compiles a shader but doesn't verify COMPILE_STATUS. While createProgram checks this later if linking fails, returning an invalid shader object could lead to confusing error messages.

This is acceptable since the best practice (per MDN link in code) is to defer shader compilation checks until link failure, which the code follows.


38-59: Robust shader/program error handling with eager cleanup.

The createProgram function follows WebGL best practices:

  • Defers shader compile status checks until link failure
  • Eagerly deletes shader objects in the finally block
  • Provides meaningful error messages from info logs

186-196: Context loss/restoration handling is well-implemented.

The handler prevents default behavior to signal intent to restore, and #handleContextRestored properly re-initializes the GL program and redraws the last frame if available. This ensures rendering resumes correctly after context recovery.


229-246: dispose() cleanup is comprehensive.

The method properly:

  1. Deletes the GL program
  2. Removes event listeners (preventing memory leaks)
  3. Loses the context eagerly (per MDN best practices)
  4. Calls super.dispose() for base class cleanup

This follows the retrieved learning about explicit cleanup in stream-based code paths.

libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts (1)

1-9: New module exports look good.

The additions of auto-canvas.js and flow-control.js exports are consistent with the existing barrel pattern. Based on learnings, this package re-exports everything from the main entrypoint rather than using subpath exports.

libraries/scrcpy-decoder-webcodecs/src/video/render/auto-canvas.ts (2)

6-15: Delegate pattern implementation is clean.

The AutoCanvasRenderer properly implements VideoFrameRenderer by delegating to the inner renderer. The canvas and writable getters provide transparent access to the underlying implementation.


28-30: dispose() delegates correctly to inner renderer.

The method properly forwards disposal to the inner renderer, which handles its own cleanup including the base class cleanup chain.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (5)

145-168: Constructor options destructuring with defaults is clean.

The default values for renderer, hardwareAcceleration, and optimizeForLatency are sensible:

  • AutoCanvasRenderer() provides automatic WebGL/Bitmap selection
  • "no-preference" for hardware acceleration is a safe default
  • true for optimizeForLatency prioritizes low latency over quality, appropriate for screen mirroring

The codec-to-transform-stream mapping is exhaustive with proper error handling for unsupported codecs.


176-184: Mutating chunk in InspectStream callback.

The InspectStream callback mutates the chunk object by adding hardwareAcceleration and optimizeForLatency properties. While this works, it modifies objects flowing through the stream. This is intentional to inject decoder configuration, but worth noting for maintainability.


226-237: Options interface extends Pick<VideoDecoderConfig, ...> appropriately.

The options type correctly picks hardwareAcceleration and optimizeForLatency from the standard VideoDecoderConfig type, ensuring type compatibility with the WebCodecs API.


170-196: Pipeline errors are suppressed without consumer notification.

The .catch(noop) on line 195 swallows the promise rejection from .pipeTo(), preventing consumers from observing any errors that occur in the decoding or rendering pipeline. While errors do internally propagate through the streams (via controller.error()), the WebCodecsVideoDecoder class exposes no onError callback or error event, leaving consumers unable to detect or respond to failures.

Consider exposing an error event or callback so consumers can react to decoding and rendering failures.


214-223: Consider explicitly canceling the stream pipeline in dispose() to ensure proper cleanup.

The method cleans up #size, #pause, #renderController, and #renderer, but doesn't explicitly cancel the underlying streams. While the comment states that cleanup happens when writable ends, if the caller never closes decoder.writable (the return value of #pause.readable after pipeThrough), the stream pipeline never closes and the flush() and cancel() callbacks in VideoDecoderStream never run. This leaves the native VideoDecoder resource improperly cleaned up.

Explicitly calling .cancel() on the stream at the start of the pipeline would ensure all downstream cleanup callbacks execute, even if the caller hasn't closed the writable end.

⛔ Skipped due to learnings
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling `controller.error()` immediately clears the internal `[[flushAlgorithm]]` and `[[cancelAlgorithm]]` slots, which prevents the `flush()` and `cancel()` callbacks from being invoked. This means that if a decoder's error callback calls `controller.error()`, the stream's `cancel()` callback will not subsequently run, eliminating concerns about double-close scenarios.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fd963c and 4945bc1.

📒 Files selected for processing (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (8)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (8)

1-12: LGTM!

Clean imports and the pre-resolved promise constant is an efficient pattern for methods that complete synchronously but need to return a Promise.


14-19: LGTM!

The design correctly defers compile status checking to createProgram (only when linking fails), following WebGL best practices as documented in the MDN reference.


21-60: Good implementation following WebGL best practices.

The deferred shader compile checking and eager shader deletion align with MDN recommendations. One minor consideration: gl.createProgram() on line 33 can return null if the context is lost, which would cause attachShader to generate a WebGL error. However, since this is called during initialization (either in constructor after context validation or during context restoration), this edge case is unlikely to be hit in practice.


62-93: LGTM!

The shader sources correctly implement a simple texture rendering pipeline with proper UV coordinate mapping. The isSupported check appropriately disallows software rendering.


95-145: LGTM!

Good implementation with appropriate WebGL context options:

  • preserveDrawingBuffer and desynchronized trade-offs based on capture needs are well-documented.
  • Context validation before proceeding is proper.
  • Event listeners are correctly paired with removal in dispose().

147-184: LGTM!

The initialization correctly sets up the WebGL pipeline. The texture parameters (NEAREST filtering, CLAMP_TO_EDGE) are appropriate for video frame rendering. The WebGL1/WebGL2 conditional for mipmap support is a nice touch.


198-227: LGTM!

Solid implementation:

  • Early exit on context loss prevents WebGL errors.
  • gl.flush() ensures commands are submitted to the GPU.
  • Setting viewport on each draw correctly handles canvas resize scenarios.

229-258: LGTM!

Proper resource cleanup following MDN best practices:

  • Event listeners removed before context loss.
  • glLoseContext called eagerly to free GPU resources.
  • super.dispose() called appropriately at the end.

The Options interface is a clean extension with well-documented enableCapture option.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 12, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:
- Around line 193-200: The code is incorrectly creating an EncodedAudioChunk for
video frames; in the method that calls this.#decoder.decode(...) inside
VideoDecoderStream (or the class handling video decoding), replace new
EncodedAudioChunk({...}) with new EncodedVideoChunk({...}) and keep the same
properties (type, timestamp, duration, data); ensure EncodedVideoChunk is
available in scope (import or global) so the decoder receives the correct chunk
type for video decoding.
- Around line 163-171: The code constructs an EncodedAudioChunk for video
frames, causing runtime failures; update the construction in the call to
this.#decoder.decode to create an EncodedVideoChunk instead, keeping the same
fields (use chunk.type ?? "key", chunk.timestamp, chunk.duration, chunk.data)
and ensure any typings/imports/uses around EncodedVideoChunk are corrected
(replace EncodedAudioChunk with EncodedVideoChunk wherever this decoding path is
used so the VideoDecoder receives the proper chunk type).
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4945bc1 and 4321a73.

📒 Files selected for processing (9)
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/frame-type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
  • libraries/stream-extra/src/concat.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/index.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/utils/frame-type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
📚 Learning: 2025-08-14T11:20:01.711Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/prf/source.ts:2-10
Timestamp: 2025-08-14T11:20:01.711Z
Learning: TypeScript 5.9 introduced generic TypedArrays (e.g., Uint8Array<ArrayBuffer>, Int32Array<SharedArrayBuffer>) to preserve the distinction between ArrayBuffer and SharedArrayBuffer at the type level. The syntax Uint8Array<ArrayBuffer> is valid and recommended in TypeScript 5.9+ for type safety. This change helps prevent unsafe mixing of ArrayBuffer and SharedArrayBuffer types, especially important for WebCrypto APIs and other browser APIs that have different behavior for these buffer types.

Applied to files:

  • libraries/stream-extra/src/concat.ts
🧬 Code graph analysis (5)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (20-22)
  • Output (39-39)
libraries/media-codec/src/av1.ts (1)
  • Av1 (197-762)
libraries/scrcpy-decoder-webcodecs/src/video/utils/frame-type.ts (1)
  • convertFrameType (1-7)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (2)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (2)
  • data (33-44)
  • Config (48-48)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (1)
  • Config (24-35)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (4)
  • CodecTransformStream (7-10)
  • Input (20-22)
  • Output (39-39)
  • Config (24-35)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (2)
  • Input (227-227)
  • Output (228-230)
libraries/scrcpy-decoder-webcodecs/src/video/utils/frame-type.ts (1)
  • convertFrameType (1-7)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (3)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (2)
  • data (33-44)
  • Config (48-48)
libraries/adb-scrcpy/src/video.ts (3)
  • data (76-79)
  • data (81-84)
  • data (86-100)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (1)
  • Config (24-35)
libraries/stream-extra/src/concat.ts (1)
libraries/struct/src/buffer.ts (2)
  • buffer (126-332)
  • EmptyUint8Array (8-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (11)
libraries/stream-extra/src/concat.ts (2)

138-152: LGTM!

The function correctly handles all three input types:

  • Passthrough for existing Uint8Array
  • Proper view creation with byteOffset/byteLength for ArrayBufferView
  • Direct wrapping for raw ArrayBufferLike

The view semantics (no copy) is consistent with the existing concatUint8Arrays behavior.


154-172: LGTM!

Clean implementation that mirrors the established concatUint8Arrays pattern while supporting broader buffer types. The optimizations for 0/1 element cases and consistent use of byteLength are appropriate.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (2)

7-29: LGTM! Clean abstract TransformStream implementation.

The transform correctly distinguishes configuration vs data packets and delegates codec-specific configuration parsing to subclasses. The #configure wrapper appropriately adds the raw field for Annex B format handling.


47-49: LGTM!

The Config type correctly omits raw since it's automatically added by the base class's #configure method, preventing accidental duplication.

libraries/scrcpy-decoder-webcodecs/src/video/utils/frame-type.ts (1)

1-7: LGTM!

Simple, well-typed utility function that correctly maps the keyframe boolean to WebCodecs' EncodedVideoChunkType.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)

5-17: LGTM!

Clean implementation of H265TransformStream. The configure method correctly returns the parsed configuration with cropped dimensions as required by Edge's WebCodecs implementation.

libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (1)

8-45: LGTM! AV1 transform stream correctly handles inline configuration.

Unlike H.264/H.265, AV1's configuration is embedded in the bitstream via sequence headers. The implementation correctly:

  • Ignores separate configuration packets (not used by AV1)
  • Searches each data packet for sequence headers
  • Emits configuration when a sequence header is found, followed by the video chunk
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (2)

82-86: LGTM! Error handling correctly follows TransformStream cleanup pattern.

Per the learning about controller.error() clearing the cancel callback, the error handler appropriately calls #dispose() directly before the error propagates, ensuring resources are cleaned up regardless of whether cancel is invoked.


123-138: LGTM! Core frame-skipping logic is well-designed.

The keyframe-based reset correctly implements the PR's goal of limiting latency when the decoder can't keep up. Resetting on keyframe arrival (when queue is non-empty) bounds maximum latency to one keyframe interval while ensuring decoding can resume from the new keyframe.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (1)

5-15: LGTM!

Clean implementation mirroring H265TransformStream. The configure method correctly parses H.264 configuration and returns the required dimensions.

libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (1)

1-44: LGTM! Well-structured type definitions for the TransformStream-based codec architecture.

The type system cleanly models:

  • Input packets (configuration or timestamped data)
  • Output chunks (decoder config or video frames)
  • The optional raw buffer for Annex B codecs (H.264/H.265)

The Optional utility type properly handles the case where EncodedVideoChunkInit.type may be unknown initially.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (2)

73-87: Clarify the comment about this usage.

The comment on line 75 states "can't use this here" but the nested callbacks on lines 79 and 85 do access this. This works because those callbacks are invoked asynchronously after the constructor completes. Consider clarifying the comment to avoid confusion:

             start: (controller) => {
-                // WARN: can't use `this` here
+                // WARN: can't use `this` synchronously here,
+                // but async callbacks (output/error) are safe.

174-201: Consider consolidating the two decode branches.

The two decode calls (lines 182-189 and 193-200) differ only in whether config.raw is prepended to the data. This could be simplified:

♻️ Optional refactor
 #configureAndDecodeFirstKeyFrame(
     config: CodecTransformStream.Config,
     chunk: CodecTransformStream.VideoChunk,
 ) {
     this.#decoder.configure(config);
     this.#configured = true;

-    if (config.raw) {
-        this.#decoder.decode(
-            new EncodedVideoChunk({
-                type: "key",
-                timestamp: chunk.timestamp,
-                duration: chunk.duration!,
-                data: concatBuffers([config.raw, chunk.data]),
-            }),
-        );
-        return;
-    }
-
     this.#decoder.decode(
         new EncodedVideoChunk({
             type: "key",
             timestamp: chunk.timestamp,
             duration: chunk.duration!,
-            data: chunk.data,
+            data: config.raw
+                ? concatBuffers([config.raw, chunk.data])
+                : chunk.data,
         }),
     );
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4321a73 and e55fcad.

📒 Files selected for processing (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts
🧬 Code graph analysis (1)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (5)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • CodecTransformStream (7-10)
  • Config (24-35)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (1)
  • Config (48-48)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/stream-extra/src/concat.ts (1)
  • concatBuffers (154-172)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (6)
libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts (6)

1-9: LGTM!

The imports are appropriate and the class type signature correctly models the input (Config or VideoChunk) and output (VideoFrame) types for the transform stream.


10-29: LGTM!

The comment explaining the decoder state management is valuable. The #decoder! definite assignment is valid since the start callback assigns decoder synchronously during the super() call, and line 114 captures it afterward.


31-68: LGTM!

Good observability surface with framesDecoded, framesSkipped, and decoderResetCount getters. The #configured flag appropriately tracks whether the decoder has been configured with the current #config, which is necessary for handling reconfigurations separately from decoder state.


89-112: LGTM!

The transform/flush/cancel callbacks are well-structured. The error handler correctly performs explicit cleanup via #dispose() since controller.error() prevents cancel() from being invoked. Based on learnings, this is the correct pattern for TransformStream error handling.


163-171: Verify handling of undefined frame type after decoder is configured.

When this.#configured is true and chunk.type !== "key", this path treats undefined type as "key" (line 166). If the frame is actually a delta frame with an undefined type marker, passing it as "key" may cause the decoder to produce corrupted output or throw.

Is this a known edge case from the upstream codec transform streams, or should undefined-typed frames be rejected at this point?


203-214: LGTM!

The cleanup logic is thorough: removes the native event listener, disposes the EventEmitter, and defensively checks decoder state before closing. Using an arrow function for #handleDequeue ensures correct this binding when used as an event listener.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 12, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts:
- Around line 22-26: packet.keyframe is optional so
convertFrameType(packet.keyframe) may return undefined and end up passing an
invalid undefined `type` to EncodedVideoChunk; update the enqueue call in the
transform that calls controller.enqueue to ensure a valid type by using a
defensive fallback (e.g., set type to convertFrameType(packet.keyframe) ??
"delta") or alternatively make ScrcpyMediaStreamDataPacket.keyframe required in
CodecTransformStream.Input and update callers; reference the call site using
controller.enqueue and the convertFrameType helper to locate where to apply the
fix.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 140-172: The RedrawController callback currently returns nothing
and must explicitly return undefined to satisfy MaybePromiseLike<undefined>;
update the lambda passed to the RedrawController constructor (the anonymous
(frame) => { ... } callback where texImage2D, uniform updates, viewport and
drawArrays are done) to end with an explicit return undefined so the function
signature matches MaybePromiseLike<undefined>.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (1)

84-90: Consider rethrowing the error in redraw for consistency with draw().

In draw(), exceptions are both captured and rethrown. Here in redraw(), the error is captured but silently swallowed. While redraw() returns void (so callers can't await it), the inconsistency could make debugging harder. If this is intentional because redraw is fire-and-forget, a brief comment would clarify.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e55fcad and bd6661a.

📒 Files selected for processing (6)
  • .vscode/settings.json
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
🧬 Code graph analysis (3)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (4)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • Options (288-295)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
  • VideoFrameRenderer (4-8)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (1)
  • RedrawController (6-99)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (1)
  • createCanvas (1-9)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (4)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)
  • frame (85-99)
libraries/stream-extra/src/stream.ts (1)
  • AbortController (13-23)
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (1)
  • draw (22-26)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • draw (263-265)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (4)
  • CodecTransformStream (7-10)
  • Input (20-22)
  • Output (39-39)
  • Config (24-35)
libraries/scrcpy-decoder-webcodecs/src/video/utils/frame-type.ts (1)
  • convertFrameType (1-7)
🪛 GitHub Actions: Pull Request
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts

[error] 140-140: TS2345: Argument of type '(frame: VideoFrame) => void' is not assignable to parameter of type '(frame: VideoFrame) => MaybePromiseLike'.

🔇 Additional comments (11)
.vscode/settings.json (1)

61-67: LGTM!

The additions are valid technical terms that align with the PR's WebCodecs and WebGL context handling changes. The words are correctly placed in alphabetical order within the array.

libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (2)

29-53: LGTM - Well-structured draw method with proper frame lifecycle management.

The promise chaining ensures sequential execution, and the frame is properly closed in the finally block after drawing.


94-98: LGTM - Dispose properly cleans up resources.

The method correctly closes the last frame, aborts pending redraws, and sets an error state to prevent further operations.

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (3)

27-33: LGTM - Clean RedrawController integration.

The controller correctly handles size updates for non-external canvas modes before delegating to the abstract draw method.


62-80: LGTM - ResizeObserver callback handles both modern and fallback sizing.

The non-null assertions are safe per the ResizeObserver spec. The fallback from devicePixelContentBoxSize to contentBoxSize with manual devicePixelRatio calculation is correctly implemented.


108-116: LGTM - Comprehensive dispose implementation.

The method properly cleans up the canvas size, disconnects the ResizeObserver, and disposes the controller. This aligns with the learning about explicit cleanup in stream-based code.

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (3)

13-59: LGTM - Shader creation follows WebGL best practices.

The deferred compile status check and eager shader deletion align with MDN WebGL best practices. The error handling properly surfaces shader/program errors with info logs.


253-261: LGTM - WebGL context loss/restoration properly handled.

The preventDefault() on context lost enables restoration, and the restored handler correctly reinitializes the program and triggers a redraw.


267-284: LGTM - Thorough WebGL resource cleanup.

The dispose method properly deletes the program, removes event listeners, loses the context eagerly per MDN best practices, and delegates to the parent class for controller cleanup.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (2)

31-42: Clean configuration pattern with good documentation.

The separation between the abstract configure (codec-specific) and private #configure (adds raw data for Annex B format) is well-structured. The inline comment and spec link provide helpful context for future maintainers.


45-47: LGTM!

The namespace correctly exports a Config type that omits raw, since the base class handles adding the raw buffer internally. This provides a clean contract for subclass implementations.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 289-294: The JSDoc for the enableCapture property has a
grammatical error; update the description string for the enableCapture optional
property (in the WebGL renderer type/interface in webgl.ts) from "Enable this
option may reduce performance" to "Enabling this option may reduce performance"
so the comment reads correctly and clearly warns about potential performance
impact.
- Around line 32-35: gl.createProgram() may return null; check the returned
program before using it. After calling gl.createProgram(), verify that the
`program` is non-null and if it is null, throw or return an appropriate
error/early-exit (e.g., throw new Error("Failed to create WebGL program"))
instead of calling gl.attachShader/gl.linkProgram on a null value; then proceed
to call gl.attachShader(program, vertexShader) and gl.attachShader(program,
fragmentShader) and gl.linkProgram(program) only when `program` is valid.
- Around line 13-18: The createShader function uses a non-null assertion on
gl.createShader which can return null if the WebGL context is lost or the type
is invalid; change createShader to check the returned shader for null and handle
failure by throwing a descriptive Error (including type and gl.getError() if
helpful) or returning null, and also verify compilation succeeded with
gl.getShaderParameter(shader, gl.COMPILE_STATUS) and include
gl.getShaderInfoLog(shader) in the thrown error/log; update any callers of
createShader to handle the new error/nullable return accordingly.
🧹 Nitpick comments (2)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (2)

98-110: Nitpick: gaussian9 samples only 5 texels, not 9.

The function name suggests a 9-tap Gaussian kernel, but it only samples the center and 4 cardinal neighbors (5 taps). Consider renaming to gaussian5 or adding diagonal samples for a true 9-tap kernel.


155-166: Cache uniform locations during initialization.

getUniformLocation is called on every frame draw. While not expensive, caching these during #initialize is a WebGL best practice for better performance.

Suggested approach
 #program!: WebGLProgram;
+#texelSizeLocation!: WebGLUniformLocation | null;
+#zoomLocation!: WebGLUniformLocation | null;

 // In #initialize():
 gl.useProgram(this.#program);
+this.#texelSizeLocation = gl.getUniformLocation(this.#program, "texelSize");
+this.#zoomLocation = gl.getUniformLocation(this.#program, "zoom");

 // In RedrawController callback:
-const texelSizeLocation = gl.getUniformLocation(this.#program, "texelSize");
-gl.uniform2f(texelSizeLocation, ...);
+gl.uniform2f(this.#texelSizeLocation, ...);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd6661a and 6a12b97.

📒 Files selected for processing (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (5)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (5)

181-219: LGTM!

The constructor follows WebGL best practices with appropriate context options for video rendering. The context lost/restored event handlers are properly set up using stable arrow function references for later removal.


221-251: LGTM!

The initialization correctly sets up the WebGL pipeline. Using NEAREST filtering is appropriate since the fragment shader handles custom filtering based on zoom level.


253-261: LGTM!

Context loss handling follows the standard pattern: preventDefault() signals intent to restore, and #handleContextRestored properly reinitializes the pipeline and redraws the last frame for seamless recovery.


263-265: LGTM!

Clean delegation to the RedrawController which handles frame scheduling and the actual rendering.


267-284: Call this.#controller.dispose() to prevent resource leaks.

The #controller instance holds references to a VideoFrame and an AbortController that must be explicitly cleaned up. Its dispose() method closes the frame and aborts any pending redraw operation. Add this.#controller.dispose(); before calling super.dispose().

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 32-35: gl.createProgram() may return null; add a null check after
calling gl.createProgram() (the variable program used with gl.attachShader and
gl.linkProgram). If program is null, handle the failure immediately (e.g.,
log/processLogger.error and return or throw an Error) before calling
gl.attachShader(program, ...) or gl.linkProgram(program) to avoid silent
failures when the GL context is lost. Ensure references to program in functions
that follow are guarded so they won't be used when creation failed.
🧹 Nitpick comments (3)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (3)

13-18: Consider adding null check for shader creation.

gl.createShader() returns null if the context is lost or the type is invalid. The non-null assertion could mask issues. Consider adding a null check for defensive programming.

♻️ Suggested improvement
 function createShader(gl: WebGLRenderingContext, type: number, source: string) {
     const shader = gl.createShader(type)!;
+    if (!shader) {
+        throw new Error("Failed to create shader");
+    }
     gl.shaderSource(shader, source);
     gl.compileShader(shader);
     return shader;
 }

155-166: Cache uniform locations for better performance.

gl.getUniformLocation() is called on every frame, which adds overhead. Uniform locations don't change after program linking, so they should be cached during initialization.

♻️ Suggested improvement

Add private fields for cached locations:

#texelSizeLocation!: WebGLUniformLocation | null;
#zoomLocation!: WebGLUniformLocation | null;

Cache them in #initialize() after gl.useProgram(this.#program):

this.#texelSizeLocation = gl.getUniformLocation(this.#program, "texelSize");
this.#zoomLocation = gl.getUniformLocation(this.#program, "zoom");

Then use cached locations in the draw callback:

-        const texelSizeLocation = gl.getUniformLocation(
-            this.#program,
-            "texelSize",
-        );
-        gl.uniform2f(
-            texelSizeLocation,
+        gl.uniform2f(
+            this.#texelSizeLocation,
             1.0 / frame.codedWidth,
             1.0 / frame.codedHeight,
         );

-        const zoomLocation = gl.getUniformLocation(this.#program, "zoom");
-        gl.uniform1f(zoomLocation, this.canvas.width / frame.codedWidth);
+        gl.uniform1f(this.#zoomLocation, this.canvas.width / frame.codedWidth);

267-284: Cleanup could be more thorough.

The texture and vertex buffer created in #initialize() are not explicitly deleted. While glLoseContext() invalidates all resources, explicit deletion is cleaner and follows the eager deletion pattern used elsewhere.

Consider storing and deleting the texture and vertex buffer:

// In class fields
#texture!: WebGLTexture;
#vertexBuffer!: WebGLBuffer;

// In dispose, before glLoseContext:
this.#context.deleteTexture(this.#texture);
this.#context.deleteBuffer(this.#vertexBuffer);

This is a minor nit since glLoseContext() handles cleanup anyway.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a12b97 and da4917f.

📒 Files selected for processing (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (6)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (6)

61-135: Well-implemented shader pipeline with adaptive filtering.

Good use of:

  • Oversized triangle technique for full-canvas coverage.
  • Adaptive filtering based on zoom level (tent4 for heavy downscaling, gaussian9 for moderate, direct sampling near 1:1).
  • failIfMajorPerformanceCaveat to avoid software rendering fallback.

181-219: LGTM!

Constructor properly:

  • Configures WebGL context with appropriate options for video rendering.
  • References MDN best practices for alpha: true.
  • Sets up context loss/restoration handlers.

221-251: LGTM!

Initialization correctly sets up the WebGL program, vertex buffer, and texture with appropriate parameters for video rendering.


253-261: LGTM!

Context loss handling follows the WebGL best practice:

  • preventDefault() signals intent to restore.
  • Re-initialization recreates all resources.
  • redraw() replays the last frame.

263-265: LGTM!

Clean delegation to the RedrawController for frame management.


287-296: LGTM!

Options namespace properly extends the base options with the enableCapture flag and includes clear documentation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts:
- Around line 99-110: The close/abort handlers currently leave a queued frame in
#nextFrame unrendered (and potentially leaked) when #drawing is false; change
close to be async and, before closing the controller and disposing the counter,
call the instance method #draw() one final time (await it) to ensure any pending
#nextFrame is rendered (keep #captureFrame open for snapshot as before), and
mirror the same behavior in abort if you want to flush the last frame on abort;
ensure you await #draw() before calling this.#readableController.close() and
this.#counter.dispose() so the last frame is not leaked.
- Around line 152-158: The dispose() method currently only clears frames and
must also perform the same teardown as close()/abort(): call
this.#counter?.dispose() to stop the requestAnimationFrame loop and close/abort
any streams or controllers the class holds (the same streams that
close()/abort() closes) so no resources are leaked when dispose() is invoked
directly.
- Around line 93-98: The fire-and-forget call to this.#draw() can cause
unhandled rejections from #readableController.enqueue(); capture the Promise
returned by this.#draw() instead of using void and attach a .catch handler that
forwards errors to the stream controllers: call
this.#readableController.error(err) and also call the writable controller's
error handler (the same controller.error(...) used in the existing abort
handler) so enqueued-rejections are propagated and handled rather than becoming
unhandled promise rejections.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 308-325: The dispose() implementation omits disposing the
RedrawController, so call this.#controller.dispose() (or check for its existence
and call dispose) before calling super.dispose() to ensure the controller
releases its stored #lastFrame and other resources; locate the dispose() method
in the WebGL renderer and add the controller disposal
(this.#controller.dispose()) as part of the cleanup sequence.
- Around line 304-306: The override draw() in the WebGL renderer should stop
delegating to its private #controller and instead implement the rendering logic
inline so it uses the base class's RedrawController and single cloned frame
storage; remove the redundant RedrawController field (#controller) and any
#lastFrame duplication, then copy/move the WebGL-specific rendering sequence
currently around lines 182–210 into the override draw(frame: VideoFrame)
implementation (follow the BitmapVideoFrameRenderer pattern) so drawing uses the
base CanvasVideoFrameRenderer coordination and its RedrawController clone
behavior.
🧹 Nitpick comments (4)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)

62-81: Non-null assertions on ResizeObserver entries could fail in edge cases.

The code assumes entries[0] and the size arrays always exist with ! assertions. While ResizeObserver typically provides at least one entry when called, defensive checks would be safer.

♻️ Suggested defensive check
 this.#resizeObserver = new ResizeObserver((entries) => {
-    const entry = entries[0]!;
+    const entry = entries[0];
+    if (!entry) {
+        return;
+    }

     if (entry.devicePixelContentBoxSize) {
-        const size = entry.devicePixelContentBoxSize[0]!;
+        const size = entry.devicePixelContentBoxSize[0];
+        if (!size) {
+            return;
+        }
         this.#displayWidth = size.inlineSize;
         this.#displayHeight = size.blockSize;
     } else {
-        const size = entry.contentBoxSize[0]!;
+        const size = entry.contentBoxSize[0];
+        if (!size) {
+            return;
+        }
         this.#displayWidth = Math.round(
             size.inlineSize * devicePixelRatio,
         );

85-99: #updateSize return value is unused.

The method returns a boolean indicating whether the size changed, but the caller in #controller (lines 28-32) doesn't use this return value. Either remove the return value or use it to optimize (e.g., skip redraw if size unchanged but frame is same).

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (2)

13-18: createShader doesn't check for null return from gl.createShader().

While rare, gl.createShader() can return null if the type is invalid. The non-null assertion ! assumes this never happens, which is reasonable given the controlled usage, but a defensive check would be safer.

♻️ Suggested defensive check
 function createShader(gl: WebGLRenderingContext, type: number, source: string) {
     const shader = gl.createShader(type)!;
+    if (!shader) {
+        throw new Error(`Failed to create shader of type ${type}`);
+    }
     gl.shaderSource(shader, source);
     gl.compileShader(shader);
     return shader;
 }

196-207: getUniformLocation is called on every draw, which is inefficient.

gl.getUniformLocation() is called inside the draw callback for both texelSize and zoom uniforms. These locations are stable once the program is linked and should be cached during initialization for better performance.

♻️ Suggested optimization
 #context: WebGLRenderingContext;
 #program!: WebGLProgram;
+#texelSizeLocation!: WebGLUniformLocation | null;
+#zoomLocation!: WebGLUniformLocation | null;

 // In #initialize():
 this.#program = createProgram(...);
 gl.useProgram(this.#program);
+this.#texelSizeLocation = gl.getUniformLocation(this.#program, "texelSize");
+this.#zoomLocation = gl.getUniformLocation(this.#program, "zoom");

 // In RedrawController callback:
-const texelSizeLocation = gl.getUniformLocation(this.#program, "texelSize");
-gl.uniform2f(texelSizeLocation, ...);
+gl.uniform2f(this.#texelSizeLocation, ...);

-const zoomLocation = gl.getUniformLocation(this.#program, "zoom");
-gl.uniform1f(zoomLocation, ...);
+gl.uniform1f(this.#zoomLocation, ...);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da4917f and c184507.

📒 Files selected for processing (6)
  • libraries/adb-credential-web/src/storage/password.ts
  • libraries/adb-credential-web/src/storage/prf/storage.ts
  • libraries/media-codec/src/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-08-14T11:20:12.044Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/type.ts:1-7
Timestamp: 2025-08-14T11:20:12.044Z
Learning: The TangoDataStorage interface is only defined in libraries/adb-credential-web/src/storage/type.ts. There is no duplicate definition in libraries/adb/src/storage/type.ts as that file does not exist.

Applied to files:

  • libraries/adb-credential-web/src/storage/password.ts
  • libraries/adb-credential-web/src/storage/prf/storage.ts
📚 Learning: 2025-08-14T11:20:12.044Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/type.ts:1-7
Timestamp: 2025-08-14T11:20:12.044Z
Learning: The TangoDataStorage interface is defined in libraries/adb-credential-web/src/storage/type.ts and there is no duplicate definition in libraries/adb/src/storage/type.ts (which doesn't exist).

Applied to files:

  • libraries/adb-credential-web/src/storage/password.ts
  • libraries/adb-credential-web/src/storage/prf/storage.ts
📚 Learning: 2025-08-14T11:06:21.851Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/index.ts:1-5
Timestamp: 2025-08-14T11:06:21.851Z
Learning: The yume-chan/adb-credential-web package doesn't use "exports" in package.json for better compatibility. Instead, they re-export everything from the main entrypoint rather than using subpath exports.

Applied to files:

  • libraries/adb-credential-web/src/storage/password.ts
  • libraries/adb-credential-web/src/storage/prf/storage.ts
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
📚 Learning: 2025-08-14T11:20:01.711Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/prf/source.ts:2-10
Timestamp: 2025-08-14T11:20:01.711Z
Learning: TypeScript 5.9 introduced generic TypedArrays (e.g., Uint8Array<ArrayBuffer>, Int32Array<SharedArrayBuffer>) to preserve the distinction between ArrayBuffer and SharedArrayBuffer at the type level. The syntax Uint8Array<ArrayBuffer> is valid and recommended in TypeScript 5.9+ for type safety. This change helps prevent unsafe mixing of ArrayBuffer and SharedArrayBuffer types, especially important for WebCrypto APIs and other browser APIs that have different behavior for these buffer types.

Applied to files:

  • libraries/adb-credential-web/src/storage/prf/storage.ts
📚 Learning: 2025-09-04T14:26:19.473Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 797
File: libraries/adb/src/features.ts:1-7
Timestamp: 2025-09-04T14:26:19.473Z
Learning: The user intentionally uses TypeScript declaration merging to combine AdbFeature value and type into an enum-like object pattern, and prefers to disable Biome linting rather than rename imports.

Applied to files:

  • libraries/adb-credential-web/src/storage/prf/storage.ts
🧬 Code graph analysis (4)
libraries/adb-credential-web/src/storage/password.ts (1)
libraries/adb/src/utils/array-buffer.ts (1)
  • toLocalUint8Array (1-8)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (3)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
  • VideoFrameRenderer (4-8)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (1)
  • RedrawController (6-99)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (1)
  • createCanvas (1-9)
libraries/adb-credential-web/src/storage/prf/storage.ts (1)
libraries/adb/src/utils/array-buffer.ts (1)
  • toLocalUint8Array (1-8)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (4)
libraries/scrcpy-decoder-webcodecs/src/video/pool.ts (1)
  • Pool (1-37)
libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts (1)
  • VideoFrameCapturer (1-42)
libraries/stream-extra/src/push-readable.ts (2)
  • PushReadableStreamController (6-14)
  • PushReadableStream (41-307)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (13)
libraries/media-codec/src/index.ts (1)

2-2: LGTM!

The new format module export is correctly added, follows the existing export * pattern used for av1 and nalu, and maintains alphabetical ordering.

libraries/adb-credential-web/src/storage/password.ts (2)

2-2: LGTM!

The import addition is necessary for the conversion used on line 96.


83-97: Approve the type relaxation and conversion.

The change from Uint8Array<ArrayBuffer> to Uint8Array improves API flexibility, and toLocalUint8Array correctly ensures the buffer is suitable for the Web Crypto API.

One minor note: if toLocalUint8Array creates a copy (when the buffer isn't an ArrayBuffer), that copy won't be explicitly cleared. However, this is consistent with the existing acknowledged limitations (lines 107-110) and the copy becomes eligible for GC immediately after encrypt completes.

libraries/adb-credential-web/src/storage/prf/storage.ts (2)

1-1: LGTM!

Both imports are correctly added: toLocalUint8Array is used at line 125 for the encryption call, and MaybeError is used for the load() generator return type at line 140.


87-126: LGTM!

The type relaxation from Uint8Array<ArrayBuffer> to Uint8Array paired with the toLocalUint8Array() call is the right approach—it makes the API more flexible for callers while ensuring WebCrypto compatibility internally. Based on learnings, this aligns with TypeScript 5.9+ generic TypedArray patterns for proper ArrayBuffer/SharedArrayBuffer distinction.

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)

35-42: WritableStream error propagation is missing.

The WritableStream wires close and abort to dispose(), but there's no error handling for the underlying #controller.draw(frame) call in the write callback. If draw() rejects, the error will propagate to the stream, but there's no explicit handling. This is likely intentional since the stream will error naturally, but worth noting.

Additionally, the dispose() method doesn't return anything explicitly, yet close and abort callbacks expect undefined or a promise. Since dispose() is typed to return undefined, this works correctly.


108-116: dispose() doesn't dispose the #controller before other cleanup.

The RedrawController.dispose() is called last, but #controller could still be processing a frame when dispose() is invoked. This ordering is fine since RedrawController.dispose() sets an error state that prevents further draws, but consider whether the #writable stream should also be explicitly aborted or closed to prevent further writes.

Based on learnings, controller.error() in TransformStream clears cancel callbacks, so explicit cleanup is important. While this isn't a TransformStream, the pattern applies: ensure no dangling operations remain after dispose.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (3)

13-15: Module-level singleton pool may cause issues with multiple RendererController instances.

VideoFrameCapturerPool is initialized once at module level with capacity 4. If multiple RendererController instances call snapshot() concurrently, they share this pool. This is likely intentional for resource efficiency, but if the pool becomes exhausted, borrow() will block until a capturer is returned.


114-136: Race condition: #nextFrame could be set between while-check and undefined assignment.

In #draw(), between line 128 checking this.#nextFrame and line 129 setting it to undefined, the write callback could set a new #nextFrame. However, since JavaScript is single-threaded and await is the only yield point (line 130), this race cannot actually occur—the write callback would be blocked by the ongoing await.

The logic is correct. The while loop ensures all frames are processed even if new ones arrive during the await.


138-150: snapshot() doesn't handle closed/disposed state.

If snapshot() is called after dispose(), #captureFrame will be undefined (as dispose() sets it to undefined), so it will return undefined. However, if called during a race where dispose() is running concurrently, the frame could be closed while capture() is using it.

Since close() and abort() intentionally don't close #captureFrame, this allows post-close snapshots. But dispose() closes it, so callers should be aware that dispose() invalidates snapshot capability.

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (3)

53-58: Shader deletion in finally block follows WebGL best practices.

Deleting shaders after linking is correct—once attached and linked, the program holds references, and explicit deletion marks them for garbage collection when the program is deleted. This follows MDN's "delete objects eagerly" guidance.


181-213: RedrawController callback correctly handles context loss.

The early return on gl.isContextLost() prevents WebGL calls on a lost context, which would otherwise throw errors or be no-ops. The #handleContextRestored callback re-initializes and triggers a redraw, which is the correct pattern.


294-302: Context restoration doesn't delete the old program before re-initialization.

When #handleContextRestored calls #initialize(), a new program is created. The old #program reference becomes stale (the context was lost, so the old program is already invalid), but assigning over it is fine. This is correct behavior since context loss invalidates all WebGL objects.

Comment on lines 308 to 325
override dispose(): undefined {
this.#context.deleteProgram(this.#program);

this.canvas.removeEventListener(
"webglcontextlost",
this.#handleContextLost,
);
this.canvas.removeEventListener(
"webglcontextrestored",
this.#handleContextRestored,
);

// Lose contexts eagerly
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly
glLoseContext(this.#context);
return undefined;

super.dispose();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

dispose() cleans up properly and calls super.dispose().

The disposal sequence is correct:

  1. Delete the WebGL program
  2. Remove event listeners
  3. Lose the WebGL context (following MDN best practices)
  4. Call super.dispose() for base class cleanup

However, #controller.dispose() is not called. The WebGL renderer's RedrawController should also be disposed to close its stored #lastFrame.

🐛 Suggested fix
 override dispose(): undefined {
     this.#context.deleteProgram(this.#program);

     this.canvas.removeEventListener(
         "webglcontextlost",
         this.#handleContextLost,
     );
     this.canvas.removeEventListener(
         "webglcontextrestored",
         this.#handleContextRestored,
     );

+    this.#controller.dispose();
+
     // Lose contexts eagerly
     // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly
     glLoseContext(this.#context);

     super.dispose();
 }
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts around lines
308 - 325, The dispose() implementation omits disposing the RedrawController, so
call this.#controller.dispose() (or check for its existence and call dispose)
before calling super.dispose() to ensure the controller releases its stored
#lastFrame and other resources; locate the dispose() method in the WebGL
renderer and add the controller disposal (this.#controller.dispose()) as part of
the cleanup sequence.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@libraries/stream-extra/src/push-readable.ts`:
- Around line 261-270: The catch currently replaces the original rejection with
new Error("") losing error info; update the else branch that sets ready =
promise.catch(...) so it preserves and propagates the original error (e.g.,
ready = promise.catch(err => { throw err; }) or simply remove the catch and set
ready = promise) — locate the ready variable assignment inside the enqueue logic
that wraps enqueue(chunk) and change the catch to re-throw the caught error or
drop the catch entirely so rejection reasons are not discarded.
🧹 Nitpick comments (2)
libraries/scrcpy/src/2_0/impl/parse-audio-stream-metadata.ts (1)

84-93: Good modernization to for await...of iteration.

The switch to async iteration is cleaner and more idiomatic than a manual reader loop.

Minor nit: stream on line 88 shadows the function parameter on line 13. Consider renaming to innerStream or releasedStream for clarity.

Optional: Rename to avoid shadowing
-            const stream = buffered.release();
+            const releasedStream = buffered.release();

-            for await (const chunk of stream) {
+            for await (const chunk of releasedStream) {
                 await controller.enqueue(chunk);
             }
libraries/stream-extra/src/push-readable.spec.ts (1)

690-752: Good test coverage for pending enqueue handling.

This test validates that when the source Promise resolves (triggering implicit close), any pending enqueue calls are properly woken up and ignored. The test name "should cancel pending enqueue" could be slightly misleading since it tests implicit close behavior rather than consumer cancel—consider renaming to "should ignore pending enqueue on implicit close" for clarity.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c184507 and 24fefdd.

📒 Files selected for processing (6)
  • libraries/adb/src/daemon/dispatcher.ts
  • libraries/android-bin/src/bug-report.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy/src/2_0/impl/parse-audio-stream-metadata.ts
  • libraries/stream-extra/src/push-readable.spec.ts
  • libraries/stream-extra/src/push-readable.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-12T06:58:27.724Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:27.724Z
Learning: In TransformStream implementations, calling `controller.error()` immediately clears the internal `[[flushAlgorithm]]` and `[[cancelAlgorithm]]` slots, which prevents the `flush()` and `cancel()` callbacks from being invoked. This means that if a decoder's error callback calls `controller.error()`, the stream's `cancel()` callback will not subsequently run, eliminating concerns about double-close scenarios.

Applied to files:

  • libraries/android-bin/src/bug-report.ts
  • libraries/stream-extra/src/push-readable.ts
  • libraries/stream-extra/src/push-readable.spec.ts
🧬 Code graph analysis (4)
libraries/adb/src/daemon/dispatcher.ts (2)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
libraries/adb/src/daemon/packet.ts (2)
  • AdbCommand (5-12)
  • AdbCommand (14-14)
libraries/stream-extra/src/push-readable.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • AbortController (13-23)
libraries/stream-extra/src/duplex.ts (1)
  • close (115-131)
libraries/stream-extra/src/pipe-from.ts (1)
  • close (23-26)
libraries/stream-extra/src/consumable.ts (1)
  • error (40-42)
libraries/scrcpy/src/2_0/impl/parse-audio-stream-metadata.ts (2)
libraries/adb-scrcpy/src/video.ts (1)
  • stream (22-24)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
libraries/stream-extra/src/push-readable.spec.ts (1)
libraries/stream-extra/src/push-readable.ts (2)
  • PushReadableLogger (28-47)
  • PushReadableStream (49-332)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (12)
libraries/android-bin/src/bug-report.ts (1)

261-264: LGTM! Abort signal propagation enables proper cancellation.

Passing controller.abortSignal to spawn allows the bugreportz process to be aborted when the consumer cancels the stream. This aligns well with the PR's streaming-based control approach.

libraries/stream-extra/src/push-readable.spec.ts (2)

37-93: LGTM! Test assertions correctly updated for new logger signature.

The mapping log.mock.calls.map((call) => call.arguments[0]) correctly extracts the single event object passed to the logger, aligning with the updated PushReadableLogger<T> type.


432-527: Good edge case coverage for concurrent enqueue calls.

This test properly validates that multiple non-awaited enqueue calls with highWaterMark: 0 are handled gracefully, even though this pattern isn't recommended for production use. The step-by-step verification with resetCalls() makes the test clear and maintainable.

libraries/stream-extra/src/push-readable.ts (7)

14-22: API change: enqueue now returns Promise<boolean>.

The return type change from Promise<void> to Promise<boolean> is a breaking change. The boolean indicates whether the chunk was successfully enqueued (true) or ignored due to cancellation/close (false). This is a useful improvement for producers to know if their data was accepted.


74-185: LGTM! Well-structured enqueue implementation with proper backpressure handling.

The enqueue logic correctly handles multiple cases:

  • Aborted streams return false without throwing
  • Error state delegates to controller to throw
  • Zero highWaterMark special case allows immediate enqueue when consumer is ready
  • Backpressure waits on waterMarkLow promise with proper resolve/reject handling

187-219: LGTM! Close function correctly handles explicit vs implicit close.

The conditional abortController.signal.aborted || (stopped && !explicit) properly allows explicit close on cancelled streams while preventing duplicate implicit closes. The waterMarkLow?.reject() correctly wakes pending enqueues.


221-242: LGTM! Error function is straightforward and correct.

Per the comment and retrieved learnings, controller_.error() won't throw on already-closed/errored/cancelled streams, so no conditional checks are needed. Based on learnings, calling controller.error() clears internal flush/cancel slots, which is the expected behavior.


279-289: LGTM! Source promise handling is correct.

The !stopped check before attaching promise handlers prevents redundant close/error calls if the producer already explicitly closed or errored the stream. Using close(false) and error(e, false) correctly marks these as implicit operations.


291-310: LGTM! Pull callback properly manages backpressure signals.

The pull callback correctly resolves the waterMarkLow promise to wake pending enqueues, and clears it to prevent multiple resolves. The zeroHighWaterMarkAllowEnqueue flag properly handles the special case where highWaterMark: 0 requires a different signaling mechanism.


311-327: LGTM! Cancel callback correctly coordinates cleanup.

The cancel callback properly sets stopped, aborts the controller with the cancellation reason, and rejects waterMarkLow to wake any pending enqueue operations. The order of operations ensures consistent state when the pending enqueue wakes up.

libraries/adb/src/daemon/dispatcher.ts (2)

148-156: LGTM - Fire-and-forget pattern for OPEN is safe.

The socket is only registered in #sockets after the handler completes and OKAY is sent (lines 350-355), so the device won't send WRITE/CLOSE packets until this completes. The error propagation via controller.error(e) correctly terminates the connection on fatal errors.


157-165: Fire-and-forget WRITE handling is safe; concurrent enqueues are serialized.

The concern about concurrent WRITE packets causing out-of-order data is mitigated by the PushReadableStreamController implementation. Its enqueue() method internally serializes concurrent calls through a ready promise chain (see push-readable.ts lines 251–268): each subsequent call awaits the previous one, ensuring FIFO ordering regardless of when #handleWrite() tasks complete.

No action needed.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts`:
- Around line 238-247: The dispose() method must explicitly close the
VideoDecoder stream to avoid leaking hardware resources: ensure you force-close
or abort this.#rawDecoder's writable stream before disposing other helpers so
the VideoDecoderStream flush/cancel callbacks run; call
this.#rawDecoder.writable.getWriter().close() (or
this.#rawDecoder.writable.getWriter().abort() on error) and await it (or handle
promise) so the underlying VideoDecoder is closed, then proceed to call
this.#size.dispose(), this.#pause.dispose(), this.#renderController.dispose(),
and this.#renderer.dispose?.().
♻️ Duplicate comments (5)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (2)

100-111: Resource leak: #nextFrame not closed on close() and abort().

The comments state "Don't close #nextFrame to make sure all frames are rendered," but after close() or abort(), no more frames will be rendered. If #nextFrame holds a VideoFrame when the stream terminates, it will leak.

🔧 Suggested fix
             close: () => {
+                this.#nextFrame?.close();
+                this.#nextFrame = undefined;
                 this.#readableController.close();
                 this.#counter.dispose();
                 // Don't close `#captureFrame` to allow using `snapshot` on the last frame
-                // Don't close `#nextFrame` to make sure all frames are rendered
             },
             abort: (reason) => {
+                this.#nextFrame?.close();
+                this.#nextFrame = undefined;
                 this.#readableController.error(reason);
                 this.#counter.dispose();
                 // Don't close `#captureFrame` to allow using `snapshot` on the last frame
-                // Don't close `#nextFrame` to make sure all frames are rendered
             },

142-154: dispose() doesn't dispose #counter, potentially leaking the requestAnimationFrame loop.

The #counter is disposed in close() and abort() callbacks, but if dispose() is called directly without going through stream close/abort, the counter won't be disposed.

🔧 Suggested fix
     dispose() {
         this.#captureFrame?.close();
         this.#captureFrame = undefined;

         this.#nextFrame?.close();
         this.#nextFrame = undefined;

+        this.#counter.dispose();
+
         this.#readableController.close();
         // Throw a similar error to native TransformStream
         this.#writableController.error(
             new TypeError("The transform stream has been terminated"),
         );
     }
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (2)

12-17: Missing null check for gl.createShader().

gl.createShader() can return null if the context is lost or the type is invalid. The non-null assertion masks this, causing subsequent calls to fail with cryptic errors.

🔧 Suggested fix
 function createShader(gl: WebGLRenderingContext, type: number, source: string) {
-    const shader = gl.createShader(type)!;
+    const shader = gl.createShader(type);
+    if (!shader) {
+        throw new Error("Failed to create shader");
+    }
     gl.shaderSource(shader, source);
     gl.compileShader(shader);
     return shader;
 }

31-34: Missing null check for gl.createProgram().

gl.createProgram() can return null if the context is lost. Calling gl.attachShader(null, ...) will silently fail.

🔧 Suggested fix
     const program = gl.createProgram();
+    if (!program) {
+        gl.deleteShader(vertexShader);
+        gl.deleteShader(fragmentShader);
+        throw new Error("Failed to create program");
+    }
     gl.attachShader(program, vertexShader);
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)

174-199: Critical: Pipeline errors are silently swallowed.

The .catch(noop) at line 199 suppresses all errors from the decoding/rendering pipeline. Based on learnings, configuration failures and decoder errors should propagate to allow the client to handle them appropriately. Additionally, since controller.error() in TransformStreams clears the cancel algorithm, error handling paths cannot rely on cancel() for cleanup—making silent error swallowing even more problematic as cleanup may be incomplete.

Consider exposing errors via a callback in Options or at minimum logging them:

🔧 Proposed fix
 export namespace WebCodecsVideoDecoder {
     export interface Options extends Pick<
         VideoDecoderConfig,
         "hardwareAcceleration" | "optimizeForLatency"
     > {
         /**
          * The video codec to decode
          */
         codec: ScrcpyVideoCodecId;

         renderer?: VideoFrameRenderer | undefined;
+
+        /**
+         * Called when the pipeline encounters an error.
+         * If not provided, errors are logged to console.
+         */
+        onError?: (error: unknown) => void;
     }
 }

In constructor:

+        const handleError = options.onError ?? ((error: unknown) => {
+            console.error("Video pipeline error:", error);
+        });
+
         void this.#pause.readable
             // ... pipeline stages ...
             .pipeTo(renderer.writable)
-            // Errors will be handled by source stream
-            .catch(noop);
+            .catch(handleError);
🧹 Nitpick comments (3)
libraries/stream-extra/src/push-readable.ts (1)

210-211: Consider clearing waterMarkLow after rejection for consistency.

In pull (line 296), waterMarkLow is cleared after resolution. For consistency, you could also clear it after rejection in close, error, and cancel. This isn't functionally necessary since stopped=true prevents new waiters, but it would make the lifecycle slightly cleaner.

♻️ Suggested consistency improvement
             stopped = true;
             controller_.close();
             // Wake up pending `enqueue`
             waterMarkLow?.reject();
+            waterMarkLow = undefined;

(Similar change in error at line 234 and cancel at line 316)

libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (1)

79-94: Inconsistent error handling between draw() and redraw().

In draw() (lines 47-49), errors are captured and re-thrown. In redraw() (line 90), errors are captured but silently swallowed. This means:

  • draw() failures propagate to callers
  • redraw() failures are only stored for the next operation

If this is intentional (redraw failures shouldn't abort the caller since redraw() returns void), consider adding a brief comment explaining the design choice.

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

69-84: Non-null assertions on ResizeObserver entry arrays.

Lines 70, 73, and 77 use non-null assertions (!) on array access. While ResizeObserver typically provides at least one entry per observed element, the devicePixelContentBoxSize and contentBoxSize arrays could theoretically be empty in edge cases.

Consider defensive checks or document why these are safe:

const entry = entries[0];
if (!entry) return;

const size = entry.devicePixelContentBoxSize?.[0] ?? entry.contentBoxSize?.[0];
if (!size) return;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24fefdd and e2d0195.

📒 Files selected for processing (10)
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/utils/snapshot.ts
  • libraries/stream-extra/src/push-readable.ts
💤 Files with no reviewable changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-12T06:58:19.519Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:19.519Z
Learning: In TransformStream implementations, calling controller.error() immediately clears the internal [[flushAlgorithm]] and [[cancelAlgorithm]] slots, which prevents the flush() and cancel() callbacks from being invoked. Therefore, if a decoder’s error callback calls controller.error(), the stream’s cancel() callback will not run later. Treat this as a सच rule: do not rely on error() to trigger cleanup through cancel; instead, perform explicit cleanup and, if needed, initiate cancel() or reject the downstream promise yourself. Apply this guidance broadly to all TransformStream-based code paths (not just this file) and ensure error handling paths explicitly manage resource cleanup and stream termination without assuming cancel will be invoked.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/utils/snapshot.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
📚 Learning: 2026-01-12T06:58:27.724Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 827
File: libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts:105-120
Timestamp: 2026-01-12T06:58:27.724Z
Learning: In TransformStream implementations, calling `controller.error()` immediately clears the internal `[[flushAlgorithm]]` and `[[cancelAlgorithm]]` slots, which prevents the `flush()` and `cancel()` callbacks from being invoked. This means that if a decoder's error callback calls `controller.error()`, the stream's `cancel()` callback will not subsequently run, eliminating concerns about double-close scenarios.

Applied to files:

  • libraries/stream-extra/src/push-readable.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (4)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (4)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
  • VideoFrameRenderer (4-10)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (2)
  • RedrawController (6-102)
  • draw (32-56)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (1)
  • createCanvas (1-9)
libraries/scrcpy-decoder-webcodecs/src/video/utils/snapshot.ts (1)
  • canvasToBlob (1-17)
libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (3)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)
  • frame (92-106)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)
  • frame (115-140)
libraries/stream-extra/src/stream.ts (1)
  • AbortController (13-23)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
libraries/stream-extra/src/consumable.ts (1)
  • WritableStream (71-71)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (2)
  • glCreateContext (11-59)
  • glLoseContext (61-67)
🔇 Additional comments (25)
libraries/scrcpy-decoder-webcodecs/src/video/utils/snapshot.ts (1)

1-17: LGTM!

Clean utility function with proper handling for both canvas types. The Promise wrapper for HTMLCanvasElement.toBlob correctly handles the null case (which can occur when canvas is tainted or conversion fails), and the type narrowing via instanceof OffscreenCanvas is idiomatic.

libraries/stream-extra/src/push-readable.ts (7)

14-22: LGTM! Clean interface update for backpressure signaling.

The enqueue return type change from Promise<void> to Promise<boolean> is a well-designed API improvement. Returning false when enqueue is ignored (due to abort) allows producers to react appropriately without needing exception handling for expected flow control scenarios.


62-72: LGTM! Clean state management setup.

The separation between stopped (producer-initiated) and abortController.signal.aborted (consumer-initiated) is well-designed. The ready promise chain for serializing enqueue calls and waterMarkLow for backpressure signaling are appropriate primitives.


74-185: LGTM! Well-structured backpressure handling.

The enqueue logic correctly handles:

  • Aborted state with graceful false return
  • Error state detection via desiredSize === null
  • Special zeroHighWaterMark case
  • Backpressure waiting with proper rejection handling

The detailed comments explaining each case are valuable for maintainability.


187-219: LGTM! Proper lifecycle management.

The close logic correctly:

  • Differentiates explicit (producer-called) vs implicit (promise-resolved) close
  • Wakes pending enqueue via waterMarkLow?.reject()
  • Handles already-stopped states gracefully

221-242: LGTM! Robust error handling.

The error function correctly sets terminal state unconditionally. The comment about controller.error not throwing on closed/errored/cancelled streams is helpful documentation.


251-265: LGTM! Clever optimization for synchronous enqueues.

The logic correctly avoids unnecessary promise chaining when enqueues complete synchronously. The async keyword ensures the interface contract (Promise<boolean>) is satisfied while the internal optimization prevents extra microtasks.


287-323: LGTM! Correct consumer-side handling.

The pull callback properly signals when the consumer is ready to receive data, handling both the normal backpressure case (waterMarkLow resolution) and the zero highWaterMark edge case. The cancel callback correctly terminates all pending operations.

libraries/scrcpy-decoder-webcodecs/src/video/render/redraw.ts (1)

1-20: LGTM on the overall design.

The RedrawController effectively coordinates drawing and redrawing with proper frame lifecycle management, promise chaining for sequential operations, and cancellation support via AbortController.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)

65-113: Well-structured stream coordination with frame-skipping logic.

The implementation correctly:

  • Clones frames for capture vs rendering (#captureFrame and #nextFrame are separate)
  • Skips intermediate frames when the renderer can't keep up (lines 88-92)
  • Propagates abort signals from readable to writable (lines 74-77)
  • Uses a non-blocking draw pattern to avoid backpressure issues
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)

4-10: LGTM - clean stream-based interface.

The VideoFrameRenderer interface is well-designed:

  • writable property uses correct spelling (consistent with standard stream APIs)
  • Optional snapshot() and dispose() methods allow flexible implementations
  • Return types are appropriate (undefined for dispose aligns with the codebase pattern)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (3)

40-47: Verify WritableStream lifecycle when dispose() is called externally.

Both close and abort callbacks call dispose(), which is correct. However, if dispose() is called directly (not through stream termination), the WritableStream may remain in an inconsistent state since it won't be explicitly closed or errored.

Consider whether dispose() should also terminate the writable stream, or document that callers should close the stream before calling dispose().


49-90: Well-structured constructor with proper separation of concerns.

The constructor correctly:

  • Accepts a draw callback, optional canvas, and options
  • Validates canvasSize: "display" only works with HTMLCanvasElement
  • Sets up ResizeObserver only when needed
  • Triggers redraws on resize to update canvas content

138-154: Clear and helpful JSDoc for canvasSize option.

The documentation clearly explains the three modes and their requirements. This helps users understand when to use each option.

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (4)

60-162: Comprehensive shader implementation with adaptive filtering.

The fragment shader intelligently selects filtering based on zoom level:

  • Direct sampling at near 1:1 (zoom > 0.95)
  • Bicubic Mitchell-Netravali at moderate downscaling (zoom > 0.5)
  • Tent filter for heavy downscaling

This provides good quality across different display scenarios.


193-232: Clean integration with base class via draw callback.

The rendering logic is now correctly passed as a callback to the base CanvasVideoFrameRenderer, eliminating the previous issue of duplicate RedrawController instances. This is a significant architectural improvement.


306-314: Context loss handling follows best practices.

The implementation correctly:

  • Calls e.preventDefault() on context lost to signal intent to restore
  • Reinitializes WebGL state on context restored
  • Triggers redraw to restore the display

Note: If redraw() is called when lastFrame has been closed (per the retrieved learning about frame lifecycle), it will gracefully return early due to the !this.#lastFrame check in RedrawController.redraw().


323-340: Proper cleanup sequence in dispose().

The disposal correctly:

  1. Deletes the WebGL program
  2. Removes event listeners (preventing memory leaks)
  3. Loses the WebGL context eagerly (per MDN best practices)
  4. Calls super.dispose() for base class cleanup

The previous issue about disposing a separate #controller no longer applies since the class now uses the base class's controller.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (7)

1-21: LGTM!

The imports are well-organized and align with the new streaming pipeline architecture. The separation of codec transforms, rendering components, and utility streams reflects good modularity.


23-47: LGTM!

Static capability checks and parameter encapsulation are correctly implemented.


49-104: LGTM!

Good encapsulation of streaming components. The public getters cleanly expose decoder metrics (queue size, frame counts) without leaking internal implementation details.


106-144: LGTM!

Good documentation on the framesPresented getter explaining the vsync behavior and Chrome bug. The metrics exposure provides comprehensive observability for decode/render performance.


202-212: LGTM!

Clean delegation to the pause controller. The visibility tracking integration is a nice UX improvement for background tab handling.


214-236: LGTM!

The snapshot implementation correctly prioritizes the native renderer's snapshot capability and falls back to BitmapVideoFrameRenderer when unavailable. The try/finally ensures the fallback renderer is properly disposed.


250-261: LGTM!

Clean interface definition that properly leverages VideoDecoderConfig types for hardware acceleration and latency optimization settings.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 238 to 247
dispose() {
this.#captureFrame?.close();
// Most cleanup happens automatically when `writable` ends
// (in each stream's `close` callback).
// This method cleanup things that still available after `writable` ends

this.#counter.dispose();
this.#renderer.dispose();
this.#size.dispose();
this.#nextFrame?.close();

if (this.#rawDecoder.state !== "closed") {
this.#rawDecoder.close();
}

// This class doesn't need to guard against multiple dispose calls
// since most of the logic is already handled in `#pause`
this.#pause.dispose();

this.#setError(new Error("Attempt to write to a disposed decoder"));
this.#renderController.dispose();
this.#renderer.dispose?.();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check VideoDecoderStream class and its cleanup handling
rg -n "class VideoDecoderStream" libraries/scrcpy-decoder-webcodecs/src/ -A 30

# Check how `#rawDecoder` is used and initialized
rg -n "#rawDecoder" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -B 2 -A 2

# Check if dispose is called on streams in decoder.ts
rg -n "writable\.|readable\.|\.abort\|\.close" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -B 1 -A 1

# Check VideoDecoder close/stop methods
rg -n "VideoDecoder" libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts -B 2 -A 5

Repository: yume-chan/ya-webadb

Length of output: 5836


🏁 Script executed:

#!/bin/bash
# Get the flush and cancel callbacks in VideoDecoderStream
rg -n "flush:|cancel:" libraries/scrcpy-decoder-webcodecs/src/video/utils/video-decoder-stream.ts -A 10

# Check the full dispose() method in decoder.ts
sed -n '238,250p' libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

# Check if there's any writable stream handling before dispose
rg -n "this.#writable\|writable.abort\|writable.close" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -B 2 -A 2

# Check `#timestampTransforms` initialization and type
rg -n "#timestampTransforms" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -B 1 -A 1

Repository: yume-chan/ya-webadb

Length of output: 1950


🏁 Script executed:

#!/bin/bash
# Check writable stream creation and initialization
rg -n "WritableStream\|writable\s*=" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -B 2 -A 2

# Check full dispose and constructor area to understand stream lifecycle
sed -n '50,120p' libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

# Check if `#writable` is a field
rg -n "^\s*#writable" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -A 1

# Look for any abort or close calls in the entire file
rg -n "abort\|\.close()" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

Repository: yume-chan/ya-webadb

Length of output: 1698


🏁 Script executed:

#!/bin/bash
# Get the constructor
sed -n '130,210p' libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

# Check what PauseController does
rg -n "class PauseController" libraries/scrcpy-decoder-webcodecs/src/ -A 30

Repository: yume-chan/ya-webadb

Length of output: 2940


🏁 Script executed:

#!/bin/bash
# Find and examine PauseController
fd "pause" libraries/scrcpy-decoder-webcodecs/src/ -type f

# Search for PauseController class
rg -n "class PauseController" libraries/scrcpy-decoder-webcodecs/src/ -A 50

Repository: yume-chan/ya-webadb

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Find PauseController
rg -n "class PauseController" libraries/scrcpy-decoder-webcodecs/src/ -A 50

# Also search for imports to locate it
rg -n "PauseController" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

Repository: yume-chan/ya-webadb

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Check if there are tests for dispose method
fd -e test.ts -e spec.ts libraries/scrcpy-decoder-webcodecs/

# Check for any documentation or comments about dispose lifecycle
rg -n "dispose\|abort\|close" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -B 2 -A 2

# Check package.json for dependencies
cat libraries/scrcpy-decoder-webcodecs/package.json | grep -A 20 "dependencies"

# See how dispose is used in tests or examples
rg -n "\.dispose()" libraries/scrcpy-decoder-webcodecs/ -B 3 -A 3

Repository: yume-chan/ya-webadb

Length of output: 7382


🏁 Script executed:

#!/bin/bash
# Search for PauseController usage/examples
rg -n "PauseController" libraries/ -B 3 -A 3

# Check if there are integration tests
fd -e test.ts -e spec.ts libraries/scrcpy-decoder-webcodecs/

# Look for any lifecycle/cleanup tests
rg -n "dispose\|cleanup\|teardown" libraries/scrcpy-decoder-webcodecs/src/ -B 2 -A 2 | head -50

# Check the actual export/interface of WebCodecsVideoDecoder
rg -n "export.*WebCodecsVideoDecoder\|export class\|export interface" libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts -A 5

Repository: yume-chan/ya-webadb

Length of output: 7344


🏁 Script executed:

#!/bin/bash
# Get the PauseController.dispose() method
sed -n '1,223p' libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts | grep -n "dispose" -A 15

# Full view of dispose method
rg -n "dispose\(" libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts -A 20

Repository: yume-chan/ya-webadb

Length of output: 5657


🏁 Script executed:

#!/bin/bash
# Check TimestampTransforms class and if it needs dispose
rg -n "class TimestampTransforms" libraries/scrcpy-decoder-webcodecs/src/ -A 50

# Check if TimestampTransforms has dispose method
rg -n "class TimestampTransforms\|dispose()" libraries/scrcpy-decoder-webcodecs/src/video/utils/timestamp.ts -A 3

Repository: yume-chan/ya-webadb

Length of output: 5097


Explicitly close #rawDecoder (VideoDecoder) in dispose().

The dispose() method relies on stream close callbacks to clean up #rawDecoder, but if dispose() is called while the pipeline is active (e.g., user closes the UI), the writable stream is not terminated and those callbacks never fire. This leaves the underlying VideoDecoder unclosed and hardware decoder resources potentially leaked. Call this.#rawDecoder.writable.getWriter().close() or abort the stream to ensure VideoDecoderStream's flush/cancel callbacks execute and properly close the decoder.

🤖 Prompt for AI Agents
In `@libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts` around lines 238 -
247, The dispose() method must explicitly close the VideoDecoder stream to avoid
leaking hardware resources: ensure you force-close or abort this.#rawDecoder's
writable stream before disposing other helpers so the VideoDecoderStream
flush/cancel callbacks run; call this.#rawDecoder.writable.getWriter().close()
(or this.#rawDecoder.writable.getWriter().abort() on error) and await it (or
handle promise) so the underlying VideoDecoder is closed, then proceed to call
this.#size.dispose(), this.#pause.dispose(), this.#renderController.dispose(),
and this.#renderer.dispose?.().

@yume-chan yume-chan closed this Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants